Merge pull request #37454 from openedx/feanil/drop_course_home

feat!: Drop the legacy studio course home page
This commit is contained in:
Feanil Patel
2025-10-20 11:45:58 -04:00
committed by GitHub
11 changed files with 56 additions and 1287 deletions

View File

@@ -40,9 +40,15 @@ class CourseWaffleFlagsSerializer(serializers.Serializer):
def get_use_new_home_page(self, obj):
"""
Method to get the use_new_home_page switch
Method to indicate whether we should use the new home page.
This used to be based on a waffle flag but the flag is being removed so we
default it to true for now until we can remove the need for it from the consumers
of this serializer and the related APIs.
See https://github.com/openedx/edx-platform/issues/37497
"""
return toggles.use_new_home_page()
return True
def get_use_new_custom_pages(self, obj):
"""

View File

@@ -16,11 +16,11 @@ from unittest import SkipTest, mock
from uuid import uuid4
import ddt
import lxml.html
from django.conf import settings
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_switch, override_waffle_flag
from edxval.api import create_video, get_videos_for_course
from fs.osfs import OSFS
@@ -1388,17 +1388,6 @@ class ContentStoreTest(ContentStoreTestCase):
resp = self.client.ajax_post('/course/', self.course_data)
self.assertEqual(resp.status_code, 403)
@override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True)
def test_course_index_view_with_no_courses(self):
"""Test viewing the index page with no courses"""
resp = self.client.get_html('/home/')
self.assertContains(
resp,
f'<h1 class="page-header">{settings.STUDIO_SHORT_NAME} Home</h1>',
status_code=200,
html=True
)
def test_course_factory(self):
"""Test that the course factory works correctly."""
course = CourseFactory.create()
@@ -1879,17 +1868,21 @@ class RerunCourseTest(ContentStoreTestCase):
"""
Asserts that the given course key is NOT in the unsucceeded course action section of the html.
"""
with override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True):
course_listing = lxml.html.fromstring(self.client.get_html('/home/').content)
self.assertEqual(len(self.get_unsucceeded_course_action_elements(course_listing, course_key)), 0)
response = self.client.get(reverse('cms.djangoapps.contentstore:v2:courses'))
assert str(course_key) not in [
course["course_key"]
for course in response.json()["results"]["in_process_course_actions"]
]
def assertInUnsucceededCourseActions(self, course_key):
"""
Asserts that the given course key is in the unsucceeded course action section of the html.
"""
with override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True):
course_listing = lxml.html.fromstring(self.client.get_html('/home/').content)
self.assertEqual(len(self.get_unsucceeded_course_action_elements(course_listing, course_key)), 1)
response = self.client.get(reverse('cms.djangoapps.contentstore:v2:courses'))
assert str(course_key) in [
course["course_key"]
for course in response.json()["results"]["in_process_course_actions"]
]
def verify_rerun_course(self, source_course_key, destination_course_key, destination_display_name):
"""

View File

@@ -8,12 +8,9 @@ from unittest.mock import Mock, patch
import ddt
from ccx_keys.locator import CCXLocator
from django.conf import settings
from django.test import RequestFactory
from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys.edx.locations import CourseLocator
from cms.djangoapps.contentstore import toggles
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
from cms.djangoapps.contentstore.utils import delete_course
from cms.djangoapps.contentstore.views.course import (
@@ -89,15 +86,6 @@ class TestCourseListing(ModuleStoreTestCase):
self.client.logout()
ModuleStoreTestCase.tearDown(self) # pylint: disable=non-parent-method-called
@override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True)
def test_empty_course_listing(self):
"""
Test on empty course listing, studio name is properly displayed
"""
message = f"Are you staff on an existing {settings.STUDIO_SHORT_NAME} course?"
response = self.client.get('/home')
self.assertContains(response, message)
def test_get_course_list(self):
"""
Test getting courses with new access group format e.g. 'instructor_edx.course.run'

View File

@@ -2,10 +2,9 @@
Tests for validate Internationalization and XBlock i18n service.
"""
import gettext
from unittest import mock, skip
from unittest import mock
from django.utils import translation
from edx_toggles.toggles.testutils import override_waffle_flag
from django.utils.translation import get_language
from xblock.core import XBlock
@@ -14,10 +13,7 @@ from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE,
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
from xmodule.tests.test_export import PureXBlock
from cms.djangoapps.contentstore import toggles
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
from cms.djangoapps.contentstore.views.preview import _prepare_runtime_for_preview
from common.djangoapps.student.tests.factories import UserFactory
class FakeTranslations(XBlockI18nService):
@@ -166,101 +162,3 @@ class TestXBlockI18nService(ModuleStoreTestCase):
Test: i18n service should be callable in studio.
"""
self.assertTrue(callable(self.block.runtime._services.get('i18n'))) # pylint: disable=protected-access
class InternationalizationTest(ModuleStoreTestCase):
"""
Tests to validate Internationalization.
"""
CREATE_USER = False
def setUp(self):
"""
These tests need a user in the DB so that the django Test Client
can log them in.
They inherit from the ModuleStoreTestCase class so that the mongodb collection
will be cleared out before each test case execution and deleted
afterwards.
"""
super().setUp()
self.uname = 'testuser'
self.email = 'test+courses@edx.org'
self.password = self.TEST_PASSWORD
# Create the use so we can log them in.
self.user = UserFactory.create(username=self.uname, email=self.email, password=self.password)
# Note that we do not actually need to do anything
# for registration if we directly mark them active.
self.user.is_active = True
# Staff has access to view all courses
self.user.is_staff = True
self.user.save()
self.course_data = {
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
}
@override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True)
def test_course_plain_english(self):
"""Test viewing the index page with no courses"""
self.client = AjaxEnabledTestClient() # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.client.login(username=self.uname, password=self.password)
resp = self.client.get_html('/home/')
self.assertContains(resp,
'<h1 class="page-header">𝓢𝓽𝓾𝓭𝓲𝓸 Home</h1>',
status_code=200,
html=True)
@override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True)
def test_course_explicit_english(self):
"""Test viewing the index page with no courses"""
self.client = AjaxEnabledTestClient() # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.client.login(username=self.uname, password=self.password)
resp = self.client.get_html(
'/home/',
{},
HTTP_ACCEPT_LANGUAGE='en',
)
self.assertContains(resp,
'<h1 class="page-header">𝓢𝓽𝓾𝓭𝓲𝓸 Home</h1>',
status_code=200,
html=True)
# ****
# NOTE:
# ****
#
# This test will break when we replace this fake 'test' language
# with actual Esperanto. This test will need to be updated with
# actual Esperanto at that time.
# Test temporarily disable since it depends on creation of dummy strings
@skip
def test_course_with_accents(self):
"""Test viewing the index page with no courses"""
self.client = AjaxEnabledTestClient() # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.client.login(username=self.uname, password=self.password)
resp = self.client.get_html(
'/home/',
{},
HTTP_ACCEPT_LANGUAGE='eo'
)
TEST_STRING = (
'<h1 class="title-1">'
'My \xc7\xf6\xfcrs\xe9s L#'
'</h1>'
)
self.assertContains(resp,
TEST_STRING,
status_code=200,
html=True)

View File

@@ -11,7 +11,7 @@ Part of https://github.com/openedx/edx-platform/issues/36275.
import datetime
import time
from unittest import mock
from urllib.parse import quote_plus
from urllib.parse import quote_plus, unquote
from ddt import data, ddt, unpack
from django.conf import settings
@@ -24,6 +24,7 @@ from pytz import UTC
from cms.djangoapps.contentstore import toggles
from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, parse_json, registration, user
from cms.djangoapps.contentstore.utils import get_studio_home_url
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@@ -114,12 +115,6 @@ class AuthTestCase(ContentStoreTestCase):
# clear the cache so ratelimiting won't affect these tests
cache.clear()
def check_page_get(self, url, expected):
resp = self.client.get_html(url)
self.assertEqual(resp.status_code, expected)
return resp
@override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True)
def test_private_pages_auth(self):
"""Make sure pages that do require login work."""
auth_pages = (
@@ -143,7 +138,9 @@ class AuthTestCase(ContentStoreTestCase):
print('Not logged in')
for page in auth_pages:
print(f"Checking '{page}'")
self.check_page_get(page, expected=302)
resp = self.client.get_html(page)
assert resp.status_code == 302
assert resp.url == unquote(reverse("login", query={"next": page}))
# Logged in should work.
self.login(self.email, self.pw)
@@ -151,10 +148,11 @@ class AuthTestCase(ContentStoreTestCase):
print('Logged in')
for page in simple_auth_pages:
print(f"Checking '{page}'")
self.check_page_get(page, expected=200)
resp = self.client.get_html(page)
assert resp.status_code == 302
assert resp.url == get_studio_home_url()
@override_settings(SESSION_INACTIVITY_TIMEOUT_IN_SECONDS=1)
@override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True)
def test_inactive_session_timeout(self):
"""
Verify that an inactive session times out and redirects to the
@@ -168,7 +166,8 @@ class AuthTestCase(ContentStoreTestCase):
# make sure we can access courseware immediately
course_url = '/home/'
resp = self.client.get_html(course_url)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 302
assert resp.url == get_studio_home_url()
# then wait a bit and see if we get timed out
time.sleep(2)

View File

@@ -162,25 +162,6 @@ def individualize_anonymous_user_id(course_id):
return INDIVIDUALIZE_ANONYMOUS_USER_ID.is_enabled(course_id)
# .. toggle_name: legacy_studio.home
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Temporarily fall back to the old Studio logged-in landing page.
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2025-03-14
# .. toggle_target_removal_date: 2025-09-14
# .. toggle_tickets: https://github.com/openedx/edx-platform/issues/36275
# .. toggle_warning: In Ulmo, this toggle will be removed. Only the new (React-based) experience will be available.
LEGACY_STUDIO_HOME = WaffleFlag('legacy_studio.home', __name__)
def use_new_home_page():
"""
Returns a boolean if new studio home page mfe is enabled
"""
return not LEGACY_STUDIO_HOME.is_enabled()
# .. toggle_name: legacy_studio.custom_pages
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False

View File

@@ -15,7 +15,7 @@ from uuid import uuid4
from bs4 import BeautifulSoup
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, ValidationError
from django.urls import reverse
from django.utils import translation
from django.utils.text import Truncator
@@ -50,7 +50,6 @@ from cms.djangoapps.contentstore.toggles import (
use_new_files_uploads_page,
use_new_grading_page,
use_new_group_configurations_page,
use_new_home_page,
use_new_import_page,
use_new_schedule_details_page,
use_new_textbooks_page,
@@ -298,12 +297,15 @@ def get_studio_home_url():
"""
Gets course authoring microfrontend URL for Studio Home view.
"""
studio_home_url = None
if use_new_home_page():
mfe_base_url = settings.COURSE_AUTHORING_MICROFRONTEND_URL
if mfe_base_url:
studio_home_url = f'{mfe_base_url}/home'
return studio_home_url
mfe_base_url = settings.COURSE_AUTHORING_MICROFRONTEND_URL
if mfe_base_url:
studio_home_url = f'{mfe_base_url}/home'
return studio_home_url
raise ImproperlyConfigured(
"The COURSE_AUTHORING_MICROFRONTEND_URL must be configured. "
"Please set it to the base url for your authoring MFE."
)
def get_schedule_details_url(course_locator) -> str:

View File

@@ -14,7 +14,12 @@ from ccx_keys.locator import CCXLocator
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.core.exceptions import FieldError, PermissionDenied, ValidationError as DjangoValidationError
from django.core.exceptions import (
FieldError,
ImproperlyConfigured,
PermissionDenied,
ValidationError as DjangoValidationError,
)
from django.db.models import QuerySet
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import redirect
@@ -86,7 +91,6 @@ from ..tasks import rerun_course as rerun_course_task
from ..toggles import (
default_enable_flexible_peer_openassessments,
use_new_course_outline_page,
use_new_home_page,
use_new_updates_page,
use_new_advanced_settings_page,
use_new_grading_page,
@@ -105,8 +109,6 @@ from ..utils import (
get_grading_url,
get_group_configurations_context,
get_group_configurations_url,
get_home_context,
get_library_context,
get_lms_link_for_item,
get_proctored_exam_settings_url,
get_schedule_details_url,
@@ -652,11 +654,7 @@ def course_listing(request):
"""
List all courses and libraries available to the logged in user
"""
if use_new_home_page():
return redirect(get_studio_home_url())
home_context = get_home_context(request)
return render_to_response('index.html', home_context)
return redirect(get_studio_home_url())
@login_required
@@ -665,8 +663,14 @@ def library_listing(request):
"""
List all Libraries available to the logged in user
"""
data = get_library_context(request)
return render_to_response('index.html', data)
mfe_base_url = settings.COURSE_AUTHORING_MICROFRONTEND_URL
if mfe_base_url:
return redirect(f'{mfe_base_url}/libraries')
raise ImproperlyConfigured(
"The COURSE_AUTHORING_MICROFRONTEND_URL must be configured. "
"Please set it to the base url for your authoring MFE."
)
def _format_library_for_view(library, request, migrated_to: Optional[NamedTuple]):

View File

@@ -8,495 +8,32 @@ import json
from unittest import mock, skip
import ddt
import lxml
import pytz
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.test.utils import override_settings
from django.utils.translation import gettext as _
from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys.edx.locator import CourseLocator
from search.api import perform_search
from cms.djangoapps.contentstore import toggles
from cms.djangoapps.contentstore.courseware_index import CoursewareSearchIndexer, SearchIndexingError
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.utils import (
add_instructor,
get_proctored_exam_settings_url,
reverse_course_url,
reverse_usage_url
)
from common.djangoapps.course_action_state.managers import CourseRerunUIStateManager
from common.djangoapps.course_action_state.models import CourseRerunState
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.student.roles import CourseStaffRole, GlobalStaff, LibraryUserRole
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory, LibraryFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import BlockFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
from ..course import _deprecated_blocks_info, course_outline_initial_state, reindex_course_and_check_access
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import VisibilityState, create_xblock_info
@override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True)
@override_waffle_flag(toggles.LEGACY_STUDIO_COURSE_OUTLINE, True)
class TestCourseIndex(CourseTestCase):
"""
Unit tests for getting the list of courses and the course outline.
"""
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
def setUp(self):
"""
Add a course with odd characters in the fields
"""
super().setUp()
# had a problem where index showed course but has_access failed to retrieve it for non-staff
self.odd_course = CourseFactory.create(
org='test.org_1-2',
number='test-2.3_course',
display_name='dotted.course.name-2',
)
CourseOverviewFactory.create(
id=self.odd_course.id,
org=self.odd_course.org,
display_name=self.odd_course.display_name,
)
def check_courses_on_index(self, authed_client, expected_course_tab_len):
"""
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)
courses_tab = parsed_html.find_class('react-course-listing')
self.assertEqual(len(courses_tab), expected_course_tab_len)
def test_libraries_on_index(self):
"""
Test that the library tab is present.
"""
def _assert_library_tab_present(response):
"""
Asserts there's a library tab.
"""
parsed_html = lxml.html.fromstring(response.content)
library_tab = parsed_html.find_class('react-library-listing')
self.assertEqual(len(library_tab), 1)
# Add a library:
lib1 = LibraryFactory.create() # lint-amnesty, pylint: disable=unused-variable
index_url = '/home/'
index_response = self.client.get(index_url, {}, HTTP_ACCEPT='text/html')
_assert_library_tab_present(index_response)
# Make sure libraries are visible to non-staff users too
self.client.logout()
non_staff_user, non_staff_userpassword = self.create_non_staff_user()
lib2 = LibraryFactory.create(user_id=non_staff_user.id)
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_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_courses_on_index(self.client, 1)
def test_negative_conditions(self):
"""
Test the error conditions for the access
"""
outline_url = reverse_course_url('course_handler', self.course.id)
# register a non-staff member and try to delete the course branch
non_staff_client, _ = self.create_non_staff_authed_user_client()
response = non_staff_client.delete(outline_url, {}, HTTP_ACCEPT='application/json')
if self.course.id.deprecated:
self.assertEqual(response.status_code, 404)
else:
self.assertEqual(response.status_code, 403)
def test_course_staff_access(self):
"""
Make and register course_staff and ensure they can access the courses
"""
course_staff_client, course_staff = self.create_non_staff_authed_user_client()
for course in [self.course, self.odd_course]:
permission_url = reverse_course_url('course_team_handler', course.id, kwargs={'email': course_staff.email})
self.client.post(
permission_url,
data=json.dumps({"role": "staff"}),
content_type="application/json",
HTTP_ACCEPT="application/json",
)
# test access
self.check_courses_on_index(course_staff_client, 1)
def test_json_responses(self):
outline_url = reverse_course_url('course_handler', self.course.id)
chapter = BlockFactory.create(parent_location=self.course.location, category='chapter', display_name="Week 1")
lesson = BlockFactory.create(parent_location=chapter.location, category='sequential', display_name="Lesson 1")
subsection = BlockFactory.create(
parent_location=lesson.location,
category='vertical',
display_name='Subsection 1'
)
BlockFactory.create(parent_location=subsection.location, category="video", display_name="My Video")
resp = self.client.get(outline_url, HTTP_ACCEPT='application/json')
if self.course.id.deprecated:
self.assertEqual(resp.status_code, 404)
return
json_response = json.loads(resp.content.decode('utf-8'))
# First spot check some values in the root response
self.assertEqual(json_response['category'], 'course')
self.assertEqual(json_response['id'], str(self.course.location))
self.assertEqual(json_response['display_name'], self.course.display_name)
self.assertTrue(json_response['published'])
self.assertIsNone(json_response['visibility_state'])
# Now verify the first child
children = json_response['child_info']['children']
self.assertGreater(len(children), 0)
first_child_response = children[0]
self.assertEqual(first_child_response['category'], 'chapter')
self.assertEqual(first_child_response['id'], str(chapter.location))
self.assertEqual(first_child_response['display_name'], 'Week 1')
self.assertTrue(json_response['published'])
self.assertEqual(first_child_response['visibility_state'], VisibilityState.unscheduled)
self.assertGreater(len(first_child_response['child_info']['children']), 0)
# Finally, validate the entire response for consistency
self.assert_correct_json_response(json_response)
def test_notifications_handler_get(self):
state = CourseRerunUIStateManager.State.FAILED
action = CourseRerunUIStateManager.ACTION
should_display = True
# try when no notification exists
notification_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={
'action_state_id': 1,
})
resp = self.client.get(notification_url, HTTP_ACCEPT='application/json')
# verify that we get an empty dict out
self.assertEqual(resp.status_code, 400)
# create a test notification
rerun_state = CourseRerunState.objects.update_state(
course_key=self.course.id,
new_state=state,
allow_not_found=True
)
CourseRerunState.objects.update_should_display(
entry_id=rerun_state.id,
user=UserFactory(),
should_display=should_display
)
# try to get information on this notification
notification_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={
'action_state_id': rerun_state.id,
})
resp = self.client.get(notification_url, HTTP_ACCEPT='application/json')
json_response = json.loads(resp.content.decode('utf-8'))
self.assertEqual(json_response['state'], state)
self.assertEqual(json_response['action'], action)
self.assertEqual(json_response['should_display'], should_display)
def test_notifications_handler_dismiss(self):
state = CourseRerunUIStateManager.State.FAILED
should_display = True
rerun_course_key = CourseLocator(org='testx', course='test_course', run='test_run')
# add an instructor to this course
user2 = UserFactory()
add_instructor(rerun_course_key, self.user, user2)
# create a test notification
rerun_state = CourseRerunState.objects.update_state(
course_key=rerun_course_key,
new_state=state,
allow_not_found=True
)
CourseRerunState.objects.update_should_display(
entry_id=rerun_state.id,
user=user2,
should_display=should_display
)
# try to get information on this notification
notification_dismiss_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={
'action_state_id': rerun_state.id,
})
resp = self.client.delete(notification_dismiss_url)
self.assertEqual(resp.status_code, 200)
with self.assertRaises(CourseRerunState.DoesNotExist):
# delete nofications that are dismissed
CourseRerunState.objects.get(id=rerun_state.id)
self.assertFalse(has_course_author_access(user2, rerun_course_key))
def assert_correct_json_response(self, json_response):
"""
Asserts that the JSON response is syntactically consistent
"""
self.assertIsNotNone(json_response['display_name'])
self.assertIsNotNone(json_response['id'])
self.assertIsNotNone(json_response['category'])
self.assertTrue(json_response['published'])
if json_response.get('child_info', None):
for child_response in json_response['child_info']['children']:
self.assert_correct_json_response(child_response)
def test_course_updates_invalid_url(self):
"""
Tests the error conditions for the invalid course updates URL.
"""
# Testing the response code by passing slash separated course id whose format is valid but no course
# having this id exists.
invalid_course_key = f'{self.course.id}_blah_blah_blah'
course_updates_url = reverse_course_url('course_info_handler', invalid_course_key)
response = self.client.get(course_updates_url)
self.assertEqual(response.status_code, 404)
# Testing the response code by passing split course id whose format is valid but no course
# having this id exists.
split_course_key = CourseLocator(org='orgASD', course='course_01213', run='Run_0_hhh_hhh_hhh')
course_updates_url_split = reverse_course_url('course_info_handler', split_course_key)
response = self.client.get(course_updates_url_split)
self.assertEqual(response.status_code, 404)
# Testing the response by passing split course id whose format is invalid.
invalid_course_id = f'invalid.course.key/{split_course_key}'
course_updates_url_split = reverse_course_url('course_info_handler', invalid_course_id)
response = self.client.get(course_updates_url_split)
self.assertEqual(response.status_code, 404)
def test_course_index_invalid_url(self):
"""
Tests the error conditions for the invalid course index URL.
"""
# Testing the response code by passing slash separated course key, no course
# having this key exists.
invalid_course_key = f'{self.course.id}_some_invalid_run'
course_outline_url = reverse_course_url('course_handler', invalid_course_key)
response = self.client.get_html(course_outline_url)
self.assertEqual(response.status_code, 404)
# Testing the response code by passing split course key, no course
# having this key exists.
split_course_key = CourseLocator(org='invalid_org', course='course_01111', run='Run_0_invalid')
course_outline_url_split = reverse_course_url('course_handler', split_course_key)
response = self.client.get_html(course_outline_url_split)
self.assertEqual(response.status_code, 404)
def test_course_outline_with_display_course_number_as_none(self):
"""
Tests course outline when 'display_coursenumber' field is none.
"""
# Change 'display_coursenumber' field to None and update the course.
self.course.display_coursenumber = None
updated_course = self.update_course(self.course, self.user.id)
# Assert that 'display_coursenumber' field has been changed successfully.
self.assertEqual(updated_course.display_coursenumber, None)
# Perform GET request on course outline url with the course id.
course_outline_url = reverse_course_url('course_handler', updated_course.id)
response = self.client.get_html(course_outline_url)
# course_handler raise 404 for old mongo course
if self.course.id.deprecated:
self.assertEqual(response.status_code, 404)
return
# Assert that response code is 200.
self.assertEqual(response.status_code, 200)
# Assert that 'display_course_number' is being set to "" (as display_coursenumber was None).
self.assertContains(response, 'display_course_number: ""')
@override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True)
@ddt.ddt
class TestCourseIndexArchived(CourseTestCase):
"""
Unit tests for testing the course index list when there are archived courses.
"""
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
NOW = datetime.datetime.now(pytz.utc)
DAY = datetime.timedelta(days=1)
YESTERDAY = NOW - DAY
TOMORROW = NOW + DAY
ORG = 'MyOrg'
ENABLE_SEPARATE_ARCHIVED_COURSES = settings.FEATURES.copy()
ENABLE_SEPARATE_ARCHIVED_COURSES['ENABLE_SEPARATE_ARCHIVED_COURSES'] = True
DISABLE_SEPARATE_ARCHIVED_COURSES = settings.FEATURES.copy()
DISABLE_SEPARATE_ARCHIVED_COURSES['ENABLE_SEPARATE_ARCHIVED_COURSES'] = False
def setUp(self):
"""
Add courses with the end date set to various values
"""
super().setUp()
# Base course has no end date (so is active)
self.course.end = None
self.course.display_name = 'Active Course 1'
self.ORG = self.course.location.org
self.save_course()
CourseOverviewFactory.create(id=self.course.id, org=self.ORG)
# Active course has end date set to tomorrow
self.active_course = CourseFactory.create(
display_name='Active Course 2',
org=self.ORG,
end=self.TOMORROW,
)
CourseOverviewFactory.create(
id=self.active_course.id,
org=self.ORG,
end=self.TOMORROW,
)
# Archived course has end date set to yesterday
self.archived_course = CourseFactory.create(
display_name='Archived Course',
org=self.ORG,
end=self.YESTERDAY,
)
CourseOverviewFactory.create(
id=self.archived_course.id,
org=self.ORG,
end=self.YESTERDAY,
)
# Base user has global staff access
self.assertTrue(GlobalStaff().has_user(self.user))
# Staff user just has course staff access
self.staff, self.staff_password = self.create_non_staff_user()
for course in (self.course, self.active_course, self.archived_course):
CourseStaffRole(course.id).add_users(self.staff)
def check_index_page_with_query_count(self, separate_archived_courses, org, mongo_queries, sql_queries):
"""
Checks the index page, and ensures the number of database queries is as expected.
"""
with self.assertNumQueries(sql_queries, table_ignorelist=WAFFLE_TABLES):
with check_mongo_calls(mongo_queries):
self.check_index_page(separate_archived_courses=separate_archived_courses, org=org)
def check_index_page(self, separate_archived_courses, org):
"""
Ensure that the index page displays the archived courses as expected.
"""
index_url = '/home/'
index_params = {}
if org is not None:
index_params['org'] = org
index_response = self.client.get(index_url, index_params, HTTP_ACCEPT='text/html')
self.assertEqual(index_response.status_code, 200)
parsed_html = lxml.html.fromstring(index_response.content)
course_tab = parsed_html.find_class('courses')
self.assertEqual(len(course_tab), 1)
archived_course_tab = parsed_html.find_class('archived-courses')
self.assertEqual(len(archived_course_tab), 1 if separate_archived_courses else 0)
@ddt.data(
# Staff user has course staff access
(True, 'staff', None, 23),
(False, 'staff', None, 23),
# Base user has global staff access
(True, 'user', ORG, 23),
(False, 'user', ORG, 23),
(True, 'user', None, 23),
(False, 'user', None, 23),
)
@ddt.unpack
def test_separate_archived_courses(self, separate_archived_courses, username, org, sql_queries):
"""
Ensure that archived courses are shown as expected for all user types, when the feature is enabled/disabled.
Also ensure that enabling the feature does not adversely affect the database query count.
"""
# Authenticate the requested user
user = getattr(self, username)
password = getattr(self, username + '_password')
self.client.login(username=user, password=password)
# Enable/disable the feature before viewing the index page.
features = settings.FEATURES.copy()
features['ENABLE_SEPARATE_ARCHIVED_COURSES'] = separate_archived_courses
with override_settings(FEATURES=features):
self.check_index_page_with_query_count(separate_archived_courses=separate_archived_courses,
org=org,
mongo_queries=0,
sql_queries=sql_queries)
@ddt.data(
# Staff user has course staff access
(True, 'staff', None, 23),
(False, 'staff', None, 23),
# Base user has global staff access
(True, 'user', ORG, 23),
(False, 'user', ORG, 23),
(True, 'user', None, 23),
(False, 'user', None, 23),
)
@ddt.unpack
def test_separate_archived_courses_with_home_page_course_v2_api(
self,
separate_archived_courses,
username,
org,
sql_queries
):
"""
Ensure that archived courses are shown as expected for all user types, when the feature is enabled/disabled.
Also ensure that enabling the feature does not adversely affect the database query count.
"""
# Authenticate the requested user
user = getattr(self, username)
password = getattr(self, username + '_password')
self.client.login(username=user, password=password)
# Enable/disable the feature before viewing the index page.
features = settings.FEATURES.copy()
features['ENABLE_SEPARATE_ARCHIVED_COURSES'] = separate_archived_courses
with override_settings(FEATURES=features):
self.check_index_page_with_query_count(separate_archived_courses=separate_archived_courses,
org=org,
mongo_queries=0,
sql_queries=sql_queries)
@override_waffle_flag(toggles.LEGACY_STUDIO_COURSE_OUTLINE, True)
@ddt.ddt
class TestCourseOutline(CourseTestCase):

View File

@@ -1,629 +0,0 @@
<%page expression_filter="h"/>
<%!
from django.utils.translation import gettext as _
from django.urls import reverse
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>
<%block name="requirejs">
require(["js/factories/index"], function (IndexFactory) {
IndexFactory();
});
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions">
<h1 class="page-header">${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)}</h1>
% if user.is_active:
<nav class="nav-actions" aria-label="${_('Page Actions')}">
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
<li class="nav-item">
% if course_creator_status=='granted':
<a href="#" class="button new-button new-course-button"><span class="icon fa fa-plus icon-inline" aria-hidden="true"></span>
${_("New Course")}</a>
% elif course_creator_status=='disallowed_for_this_site' and settings.FEATURES.get('STUDIO_REQUEST_EMAIL',''):
<a href="mailto:${settings.FEATURES.get('STUDIO_REQUEST_EMAIL','')}">${_("Email staff to create course")}</a>
% endif
% if show_new_library_button:
<a href="#" class="button new-button new-library-button"><span class="icon fa fa-plus icon-inline" aria-hidden="true"></span>
${_("New Library")}</a>
% endif
</li>
</ul>
</nav>
% endif
</header>
</div>
<div class="wrapper-content wrapper">
% if user.is_active:
<section class="content">
<article class="content-primary" role="main">
% if course_creator_status=='granted':
<div class="wrapper-create-element wrapper-create-course">
<form class="form-create create-course course-info" id="create-course-form" name="create-course-form">
<div class="wrap-error">
<div id="course_creation_error" name="course_creation_error" class="message message-status message-status error" role="alert">
<p>${_("Please correct the highlighted fields below.")}</p>
</div>
</div>
<div class="wrapper-form">
<h3 class="title">${_("Create a New Course")}</h3>
<fieldset>
<legend class="sr">${_("Required Information to Create a New Course")}</legend>
<ol class="list-input">
<li class="field text required" id="field-course-name">
<label for="new-course-name">${_("Course Name")}</label>
## Translators: This is an example name for a new course, seen when
## filling out the form to create a new course.
<input class="new-course-name" id="new-course-name" type="text" name="new-course-name" required placeholder="${_('e.g. Introduction to Computer Science')}" aria-describedby="tip-new-course-name tip-error-new-course-name" />
<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">
<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.
% if can_create_organizations:
<input class="new-course-org" id="new-course-org" type="text" name="new-course-org" required placeholder="${_('e.g. UniversityX or OrganizationX')}" aria-describedby="tip-new-course-org tip-error-new-course-org" />
% else:
<select class="new-course-org" id="new-course-org" name="new-course-org" required aria-describedby="tip-new-course-org tip-error-new-course-org">
% for org in allowed_organizations:
<option value="${org}">${org}</option>
% endfor
</select>
% endif
<span class="tip" id="tip-new-course-org">${Text(_("The name of the organization sponsoring the course. {strong_start}Note: The organization name is part of the course URL.{strong_end} This cannot be changed, but you can set a different display name in Advanced Settings later.")).format(
strong_start=HTML('<strong>'),
strong_end=HTML('</strong>'),
)}</span>
<span class="tip tip-error is-hiding" id="tip-error-new-course-org"></span>
</li>
<li class="field text required" id="field-course-number">
<label for="new-course-number">${_("Course Number")}</label>
## Translators: This is an example for the number used to identify a course,
## seen when filling out the form to create a new course. The number here is
## short for "Computer Science 101". It can contain letters but cannot contain spaces.
<input class="new-course-number" id="new-course-number" type="text" name="new-course-number" required placeholder="${_('e.g. CS101')}" aria-describedby="tip-new-course-number tip-error-new-course-number" />
<span class="tip" id="tip-new-course-number">${Text(_("The unique number that identifies your course within your organization. {strong_start}Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.{strong_end}")).format(
strong_start=HTML('<strong>'),
strong_end=HTML('</strong>'),
)}</span>
<span class="tip tip-error is-hiding" id="tip-error-new-course-number"></span>
</li>
<li class="field text required" id="field-course-run">
<label for="new-course-run">${_("Course Run")}</label>
## Translators: This is an example for the "run" used to identify different
## instances of a course, seen when filling out the form to create a new course.
<input class="new-course-run" id="new-course-run" type="text" name="new-course-run" required placeholder="${_('e.g. 2014_T1')}" aria-describedby="tip-new-course-run tip-error-new-course-run" />
<span class="tip" id="tip-new-course-run">${Text(_("The term in which your course will run. {strong_start}Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.{strong_end}")).format(
strong_start=HTML('<strong>'),
strong_end=HTML('</strong>'),
)}</span>
<span class="tip tip-error is-hiding" id="tip-error-new-course-run"></span>
</li>
</ol>
</fieldset>
</div>
<div class="actions">
<input type="hidden" value="${allow_unicode_course_id}" class="allow-unicode-course-id" />
<input type="submit" value="${_('Create')}" class="action action-primary new-course-save" />
<input type="button" value="${_('Cancel')}" class="action action-secondary action-cancel new-course-cancel" />
</div>
</form>
</div>
% endif
%if libraries_enabled and show_new_library_button:
<div class="wrapper-create-element wrapper-create-library">
<form class="form-create create-library library-info" id="create-library-form" name="create-library-form">
<div class="wrap-error">
<div id="library_creation_error" name="library_creation_error" class="message message-status message-status error" role="alert">
<p>${_("Please correct the highlighted fields below.")}</p>
</div>
</div>
<div class="wrapper-form">
<h3 class="title">${_("Create a New Library")}</h3>
<fieldset>
<legend class="sr">${_("Required Information to Create a New Library")}</legend>
<ol class="list-input">
<li class="field text required" id="field-library-name">
<label for="new-library-name">${_("Library Name")}</label>
## Translators: This is an example name for a new content library, seen when
## filling out the form to create a new library.
## (A library is a collection of content or problems.)
<input class="new-library-name" id="new-library-name" type="text" name="new-library-name" required placeholder="${_('e.g. Computer Science Problems')}" aria-describedby="tip-new-library-name tip-error-new-library-name" />
<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">
<label for="new-library-org">${_("Organization")}</label>
% if can_create_organizations:
<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" />
% else:
<select class="new-library-org" id="new-library-org" name="new-library-org" required aria-describedby="tip-new-library-org tip-error-new-library-org">
% for org in allowed_organizations_for_libraries:
<option value="${org}">${org}</option>
% endfor
</select>
% endif
<span class="tip" id="tip-new-library-org">${_("The public organization name for your library.")} ${_("This cannot be changed.")}</span>
<span class="tip tip-error is-hiding" id="tip-error-new-library-org"></span>
</li>
<li class="field text required" id="field-library-number">
<label for="new-library-number">${_("Library Code")}</label>
## Translators: This is an example for the "code" used to identify a library,
## seen when filling out the form to create a new library. This example is short
## for "Computer Science Problems". The example number may contain letters
## but must not contain spaces.
<input class="new-library-number" id="new-library-number" type="text" name="new-library-number" required placeholder="${_('e.g. CSPROB')}" aria-describedby="tip-new-library-number tip-error-new-library-number" />
<span class="tip" id="tip-new-library-number">${Text(_("The unique code that identifies this library. {strong_start}Note: This is part of your library URL, so no spaces or special characters are allowed.{strong_end} This cannot be changed.")).format(
strong_start=HTML('<strong>'),
strong_end=HTML('</strong>'),
)}</span>
<span class="tip tip-error is-hiding" id="tip-error-new-library-number"></span>
</li>
</ol>
</fieldset>
</div>
<div class="actions">
<input type="hidden" value="${allow_unicode_course_id}" class="allow-unicode-course-id" />
<input type="submit" value="${_('Create')}" class="action action-primary new-library-save" />
<input type="button" value="${_('Cancel')}" class="action action-secondary action-cancel new-library-cancel" />
</div>
</form>
</div>
% endif
%if optimization_enabled:
<div class="optimization-form">
<h2 class="title title-3">${_("Organization and Library Settings")}</h2>
<form class="form" action="/home/" method="GET">
<fieldset class="form-group">
<div class="field">
<label class="field-label">${_("Show all courses in organization:")}
<input class="field-input input-text" type="text" name="org"
placeholder="${_('For example, MITx')}"/>
</label>
</div>
</fieldset>
<div class="form-actions">
<button class="btn-brand btn-base" type="submit">${_("Submit")}</button>
</div>
</form>
</div>
%endif
<!-- STATE: processing courses -->
%if allow_course_reruns and rerun_creator_status and len(in_process_course_actions) > 0:
<div class="courses courses-processing">
<h3 class="title">${_("Courses Being Processed")}</h3>
<ul class="list-courses">
%for course_info in sorted(in_process_course_actions, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
<!-- STATE: re-run is processing -->
%if course_info['is_in_progress']:
<li class="wrapper-course has-status" data-course-key="${course_info['course_key']}">
<div class="course-item course-rerun is-processing">
<div class="course-details" href="#">
<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>
</div>
<dl class="course-status">
<dt class="label sr">${_("This course run is currently being created.")}</dt>
<dd class="value">
<span class="icon fa fa-refresh fa-spin" aria-hidden="true"></span>
## Translators: This is a status message, used to inform the user of
## what the system is doing. This status means that the user has
## requested to re-run an existing course, and the system is currently
## in the process of duplicating and configuring the existing course
## so that it can be re-run.
<span class="copy">${_("Configuring as re-run")}</span>
</dd>
</dl>
</div>
<div class="status-message">
<p class="copy">${Text(_('The new course will be added to your course list in 5-10 minutes. Return to this page or {link_start}refresh it{link_end} to update the course list. The new course will need some manual configuration.')).format(
link_start=HTML('<a href="#" class="action-reload">'),
link_end=HTML('</a>'),
)}</p>
</div>
</li>
%endif
<!-- - - - -->
<!-- STATE: re-run has error -->
%if course_info['is_failed']:
<li class="wrapper-course has-status" data-course-key="${course_info['course_key']}">
<div class="course-item course-rerun has-error">
<div class="course-details" href="#">
<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>
</div>
<dl class="course-status">
## Translators: This is a status message for the course re-runs feature.
## When a course admin indicates that a course should be re-run, the system
## needs to process the request and prepare the new course. The status of
## the process will follow this text.
<dt class="label sr">${_("This re-run processing status:")}</dt>
<dd class="value">
<span class="icon fa fa-warning" aria-hidden="true"></span>
<span class="copy">${_("Configuration Error")}</span>
</dd>
</dl>
</div>
<div class="status-message has-actions">
<p class="copy">${_("A system error occurred while your course was being processed. Please go to the original course to try the re-run again, or contact your PM for assistance.")}</p>
<ul class="status-actions">
<li class="action action-dismiss">
<a href="#" class="button dismiss-button" data-dismiss-link="${course_info['dismiss_link']}">
<span class="icon fa fa-times-circle" aria-hidden="true"></span>
<span class="button-copy">${_("Dismiss")}</span>
</a>
</li>
</ul>
</div>
</li>
%endif
%endfor
</ul>
</div>
%endif
% if libraries_enabled or archived_courses:
<ul id="course-index-tabs">
<li class="courses-tab ${ 'active' if active_tab == 'courses' else ''}">
% if split_studio_home and active_tab == 'libraries':
<a href="${reverse('home')}">${_("Courses")}</a>
% else:
<a href="#">${_("Courses")}</a>
% endif
</li>
% if archived_courses:
<li class="archived-courses-tab">
% if split_studio_home and active_tab == 'libraries':
<a href="${reverse('home') + '#archived-courses-tab' } " >${_("Archived Courses")}</a>
% else:
<a href="#" >${_("Archived Courses")}</a>
% endif
</li>
% endif
% if libraries_enabled:
<li class="libraries-tab ${ 'active' if active_tab == 'libraries' else ''}">
% if split_studio_home:
<a href="${reverse('home_library')}">${_("Libraries")}</a>
% else:
<a href="#" >${_("Libraries")}</a>
% endif
</li>
% endif
% if taxonomies_enabled:
<li><a href="${taxonomy_list_mfe_url}">${_("Taxonomies")}</li>
% endif
</ul>
% endif
%if len(courses) > 0 or optimization_enabled:
<div class="courses courses-tab react-course-listing ${ 'active' if active_tab == 'courses' else ''}">
${static.renderReact(
component="CourseOrLibraryListing",
id="react-course-listing",
props={
'items': sorted(courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''),
'linkClass': 'course-link',
'idBase': 'course',
'allowReruns': allow_course_reruns and rerun_creator_status and course_creator_status=='granted'
}
)}
</div>
%else:
<div class="notice notice-incontext notice-instruction notice-instruction-nocourses list-notices courses-tab ${ 'active' if active_tab == 'courses' else ''}">
<div class="notice-item">
<div class="msg">
<h3 class="title">${_("Are you staff on an existing {studio_name} course?").format(studio_name=settings.STUDIO_SHORT_NAME)}</h3>
<div class="copy">
<p>${_('The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.')}</p>
</div>
</div>
</div>
%if course_creator_status == "granted":
<div class="notice-item has-actions">
<div class="msg">
<h3 class="title">${_('Create Your First Course')}</h3>
<div class="copy">
<p>${_('Your new course is just a click away!')}</p>
</div>
</div>
<ul class="list-actions">
<li class="action-item">
<a href="#" class="action-primary action-create action-create-course new-course-button"><span class="icon fa fa-plus icon-inline" aria-hidden="true"></span> ${_('Create Your First Course')}</a>
</li>
</ul>
</div>
% endif
</div>
% endif
%if course_creator_status == "unrequested":
<div class="wrapper wrapper-creationrights">
<h3 class="title">
<a href="#instruction-creationrights" class="ui-toggle-control show-creationrights"><span class="label">${_('Becoming a Course Creator in {studio_name}').format(studio_name=settings.STUDIO_SHORT_NAME)}</span> <span class="icon fa fa-times-circle" aria-hidden="true"></span></a>
</h3>
<div class="notice notice-incontext notice-instruction notice-instruction-creationrights ui-toggle-target" id="instruction-creationrights">
<div class="copy">
<p>${_('{studio_name} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platform_name}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.').format(
studio_name=settings.STUDIO_NAME, platform_name=settings.PLATFORM_NAME)}</p>
</div>
<div class="status status-creationrights is-unrequested">
<h4 class="title">${_('Your Course Creator Request Status:')}</h4>
<form id="request-coursecreator" action="${request_course_creator_url}" method="post" enctype="multipart/form-data">
<div class="form-actions">
<button type="submit" id="request-coursecreator-submit" name="request-coursecreator-submit" class="action-primary action-request"><span class="icon fa fa-cog icon-inline fa fa-spin" aria-hidden="true"></span> <span class="label">${_('Request the Ability to Create Courses')}</span></button>
</div>
</form>
</div>
</div>
</div>
%elif course_creator_status == "denied":
<div class="wrapper wrapper-creationrights is-shown">
<h3 class="title">
<a href="#instruction-creationrights" class="ui-toggle-control current show-creationrights"><span class="label">${_('Your Course Creator Request Status')}</span> <span class="icon fa fa-times-circle" aria-hidden="true"></span></a>
</h3>
<div class="notice notice-incontext notice-instruction notice-instruction-creationrights ui-toggle-target" id="instruction-creationrights">
<div class="copy">
<p>${_('{studio_name} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platform_name}. Our team is has completed evaluating your request.').format(
studio_name=settings.STUDIO_NAME, platform_name=settings.PLATFORM_NAME,
)}</p>
</div>
<div class="status status-creationrights has-status is-denied">
<h4 class="title">${_('Your Course Creator Request Status:')}</h4>
<dl class="status-update">
<dt class="label">${_('Your Course Creator request is:')}</dt>
<dd class="value">
<span class="status-indicator"></span>
<span class="value-formal">${_('Denied')}</span>
<span class="value-description">${_('Your request did not meet the criteria/guidelines specified by {platform_name} Staff.').format(platform_name=settings.PLATFORM_NAME)}</span>
</dd>
</dl>
</div>
</div>
</div>
%elif course_creator_status == "pending":
<div class="wrapper wrapper-creationrights is-shown">
<h3 class="title">
<a href="#instruction-creationrights" class="ui-toggle-control current show-creationrights"><span class="label">${_('Your Course Creator Request Status')}</span> <span class="icon fa fa-times-circle" aria-hidden="true"></span></a>
</h3>
<div class="notice notice-incontext notice-instruction notice-instruction-creationrights ui-toggle-target" id="instruction-creationrights">
<div class="copy">
<p>${_('{studio_name} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platform_name}. Our team is currently evaluating your request.').format(
studio_name=settings.STUDIO_NAME, platform_name=settings.PLATFORM_NAME,
)}</p>
</div>
<div class="status status-creationrights has-status is-pending">
<h4 class="title">${_('Your Course Creator Request Status:')}</h4>
<dl class="status-update">
<dt class="label">${_('Your Course Creator request is:')}</dt>
<dd class="value">
<span class="status-indicator"></span>
<span class="value-formal">${_('Pending')}</span>
<span class="value-description">
${_('Your request is currently being reviewed by {platform_name} staff and should be updated shortly.').format(platform_name=settings.PLATFORM_NAME)}
</span>
</dd>
</dl>
</div>
</div>
</div>
% endif
%if archived_courses:
<div class="archived-courses react-archived-course-listing archived-courses-tab">
% if type(archived_courses) is list:
${static.renderReact(
component="CourseOrLibraryListing",
id="react-archived-course-listing",
props={
'items': sorted(archived_courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''),
'linkClass': 'course-link',
'idBase': 'archived',
'allowReruns': allow_course_reruns and rerun_creator_status and course_creator_status=='granted'
}
)}
% endif
</div>
%endif
%if len(libraries) > 0 or optimization_enabled:
<div class="libraries react-library-listing libraries-tab ${ 'active' if active_tab == 'libraries' else ''}">
${static.renderReact(
component="CourseOrLibraryListing",
id="react-library-listing",
props={
'items': sorted(libraries, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''),
'linkClass': 'library-link',
'idBase': 'library',
'allowReruns': allow_course_reruns and rerun_creator_status and course_creator_status=='granted'
}
)}
</div>
%else:
<div class="notice notice-incontext notice-instruction notice-instruction-nocourses list-notices libraries-tab">
<div class="notice-item">
<div class="msg">
<h3 class="title">${_("Were you expecting to see a particular library here?")}</h3>
<div class="copy">
<p>${_('The library creator must give you access to the library. Contact the library creator or administrator for the library you are helping to author.')}</p>
</div>
</div>
</div>
% if show_new_library_button:
<div class="notice-item has-actions">
<div class="msg">
<h3 class="title">${_('Create Your First Library')}</h3>
<div class="copy">
<p>${_('Libraries hold a pool of components that can be re-used across multiple courses. Create your first library with the click of a button!')}</p>
</div>
</div>
<ul class="list-actions">
<li class="action-item">
<a href="#" class="action-primary action-create new-button action-create-library new-library-button"><span class="icon fa fa-plus icon-inline" aria-hidden="true"></span> ${_('Create Your First Library')}</a>
</li>
</ul>
</div>
% endif
</div>
%endif
</article>
<aside class="content-supplementary" role="complementary">
<div class="bit">
<h3 class="title title-3">${_('New to {studio_name}?').format(studio_name=settings.STUDIO_NAME)}</h3>
<p>${_('Click Help in the upper-right corner to get more information about the {studio_name} page you are viewing. You can also use the links at the bottom of the page to access our continually updated documentation and other {studio_name} resources.').format(studio_name=settings.STUDIO_SHORT_NAME)}</p>
<ol class="list-actions">
<li class="action-item">
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank">${_("Getting Started with {studio_name}").format(studio_name=settings.STUDIO_NAME)}</a>
</li>
</ol>
</div>
% if course_creator_status=='disallowed_for_this_site' and settings.FEATURES.get('STUDIO_REQUEST_EMAIL',''):
<div class="bit">
<h3 class="title title-3">${_("Can I create courses in {studio_name}?").format(studio_name=settings.STUDIO_NAME)}</h3>
<p>${Text(_("In order to create courses in {studio_name}, you must {link_start}contact {platform_name} staff to help you create a course{link_end}.")).format(
studio_name=settings.STUDIO_NAME,
platform_name=settings.PLATFORM_NAME,
link_start=HTML('<a href="mailto:{email}">').format(email=settings.FEATURES.get('STUDIO_REQUEST_EMAIL','')),
link_end=HTML("</a>"),
)}</p>
</div>
% endif
% if course_creator_status == "unrequested":
<div class="bit">
<h3 class="title title-3">${_("Can I create courses in {studio_name}?").format(studio_name=settings.STUDIO_NAME)}</h3>
<p>${_('In order to create courses in {studio_name}, you must have course creator privileges to create your own course.').format(studio_name=settings.STUDIO_NAME)}</p>
</div>
% elif course_creator_status == "denied":
<div class="bit">
<h3 class="title title-3">${_("Can I create courses in {studio_name}?").format(studio_name=settings.STUDIO_NAME)}</h3>
<p>${Text(_("Your request to author courses in {studio_name} has been denied. Please {link_start}contact {platform_name} Staff with further questions{link_end}.")).format(
studio_name=settings.STUDIO_NAME,
platform_name=settings.PLATFORM_NAME,
link_start=HTML('<a href="mailto:{email}">').format(email=settings.TECH_SUPPORT_EMAIL),
link_end=HTML('</a>'),
)}</p>
</div>
% endif
</aside>
</section>
% else:
<section class="content">
<article class="content-primary" role="main">
<div class="introduction">
<h2 class="title">${_("Thanks for signing up, {name}!").format(name=user.username)}</h2>
</div>
<div class="notice notice-incontext notice-instruction notice-instruction-verification">
<div class="msg">
<h3 class="title">${_("We need to verify your email address")}</h3>
<div class="copy">
<p>${_('Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.').format(email=user.email)}</p>
</div>
</div>
</div>
</article>
<aside class="content-supplementary" role="complementary">
<div class="bit">
<h3 class="title title-3">${_('Need help?')}</h3>
<p>${_('Please check your Junk or Spam folders in case our email isn\'t in your INBOX. Still can\'t find the verification email? Request help via the link below.')}</p>
</div>
</aside>
</section>
%endif
</div>
</%block>

View File

@@ -16,22 +16,12 @@
<header class="primary" role="banner">
<div class="wrapper wrapper-l">
<h1 class="branding">
% if not toggles.use_new_home_page():
<a class="brand-link" href="/">
<img class="brand-image" src="${static.url('images/studio-logo.png')}" alt="${settings.STUDIO_NAME}" />
% if settings.LOGO_IMAGE_EXTRA_TEXT == 'edge':
<span class="font-italic"> <span class="tilted">|</span> EDGE</span>
% endif
</a>
% endif
% if toggles.use_new_home_page():
<a class="brand-link" href="${get_studio_home_url()}">
<img class="brand-image" src="${static.url('images/studio-logo.png')}" alt="${settings.STUDIO_NAME}" />
% if settings.LOGO_IMAGE_EXTRA_TEXT == 'edge':
<span class="font-italic"> <span class="tilted">|</span> EDGE</span>
% endif
</a>
% endif
</h1>