Merge pull request #37454 from openedx/feanil/drop_course_home
feat!: Drop the legacy studio course home page
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user