diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py index 3efb7b6226d4..f3caa6daeb55 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py @@ -40,9 +40,15 @@ def get_course_key(self): 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): """ diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 8b6aa6d2bba5..7862d60f95b7 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -16,11 +16,11 @@ 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 @@ def assert_course_permission_denied(self): 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'

{settings.STUDIO_SHORT_NAME} Home

', - status_code=200, - html=True - ) - def test_course_factory(self): """Test that the course factory works correctly.""" course = CourseFactory.create() @@ -1879,17 +1868,21 @@ def assertInCourseListing(self, course_key): """ 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): """ diff --git a/cms/djangoapps/contentstore/tests/test_course_listing.py b/cms/djangoapps/contentstore/tests/test_course_listing.py index e46b493b7b39..d256228228cb 100644 --- a/cms/djangoapps/contentstore/tests/test_course_listing.py +++ b/cms/djangoapps/contentstore/tests/test_course_listing.py @@ -8,12 +8,9 @@ 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 @@ def tearDown(self): 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' diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py index 6201253babf4..3ee991493196 100644 --- a/cms/djangoapps/contentstore/tests/test_i18n.py +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -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.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 @@ def test_i18n_service_callable(self): 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, - '

𝓒𝓽𝓾𝓭𝓲𝓸 Home

', - 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, - '

𝓒𝓽𝓾𝓭𝓲𝓸 Home

', - 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 = ( - '

' - 'My \xc7\xf6\xfcrs\xe9s L#' - '

' - ) - - self.assertContains(resp, - TEST_STRING, - status_code=200, - html=True) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 40b0f8ad4cbf..d151b1d58526 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -11,7 +11,7 @@ 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 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 @@ def setUp(self): # 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 @@ def test_private_pages_auth(self): 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 @@ def test_private_pages_auth(self): 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 @@ def test_inactive_session_timeout(self): # 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) diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py index 55ec0e4ff5dc..de05a46ef30b 100644 --- a/cms/djangoapps/contentstore/toggles.py +++ b/cms/djangoapps/contentstore/toggles.py @@ -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 diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index efd6905745b3..04ed2787685a 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -15,7 +15,7 @@ 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 @@ 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: diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index fa8769dc0cb9..e37d980efb04 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -14,7 +14,12 @@ 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 ..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 @@ 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]): diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index a22ce637fedd..c164ccc56425 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -8,495 +8,32 @@ 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): diff --git a/cms/templates/index.html b/cms/templates/index.html deleted file mode 100644 index e55859730729..000000000000 --- a/cms/templates/index.html +++ /dev/null @@ -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" %> -<%block name="title">${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)} -<%block name="bodyclass">is-signedin index view-dashboard - -<%block name="requirejs"> - require(["js/factories/index"], function (IndexFactory) { - IndexFactory(); - }); - - -<%block name="content"> -
-
-

${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)}

- - % if user.is_active: - - % endif -
-
- -
- % if user.is_active: -
-
- - % if course_creator_status=='granted': -
-
-
- -
- -
-

${_("Create a New Course")}

- -
- ${_("Required Information to Create a New Course")} - -
    -
  1. - - ## Translators: This is an example name for a new course, seen when - ## filling out the form to create a new course. - - ${_("The public display name for your course. This cannot be changed, but you can set a different display name in Advanced Settings later.")} - -
  2. -
  3. - - ## 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: - - % else: - - % endif - ${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_end=HTML(''), - )} - -
  4. - -
  5. - - ## 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. - - ${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_end=HTML(''), - )} - -
  6. - -
  7. - - ## 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. - - ${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_end=HTML(''), - )} - -
  8. -
- -
-
- -
- - - -
-
-
- - % endif - - %if libraries_enabled and show_new_library_button: -
-
-
- -
- -
-

${_("Create a New Library")}

- -
- ${_("Required Information to Create a New Library")} - -
    -
  1. - - ## 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.) - - ${_("The public display name for your library.")} - -
  2. -
  3. - - % if can_create_organizations: - - % else: - - % endif - ${_("The public organization name for your library.")} ${_("This cannot be changed.")} - -
  4. - -
  5. - - ## 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. - - ${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_end=HTML(''), - )} - -
  6. -
- -
-
- -
- - - -
-
-
- % endif - - %if optimization_enabled: -
-

${_("Organization and Library Settings")}

-
-
-
- -
-
-
- -
-
-
- %endif - - - %if allow_course_reruns and rerun_creator_status and len(in_process_course_actions) > 0: -
-

${_("Courses Being Processed")}

- -
    - %for course_info in sorted(in_process_course_actions, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''): - - %if course_info['is_in_progress']: -
  • -
    -
    -

    ${course_info['display_name']}

    - - -
    - -
    -
    ${_("This course run is currently being created.")}
    -
    - - ## 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. - ${_("Configuring as re-run")} -
    -
    -
    - -
    -

    ${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(''), - link_end=HTML(''), - )}

    -
    -
  • - %endif - - - - - %if course_info['is_failed']: -
  • -
    -
    -

    ${course_info['display_name']}

    - - -
    - -
    - ## 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. -
    ${_("This re-run processing status:")}
    -
    - - ${_("Configuration Error")} -
    -
    -
    - -
    -

    ${_("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.")}

    - - -
    -
  • - %endif - %endfor -
-
- %endif - - % if libraries_enabled or archived_courses: - - % endif - - %if len(courses) > 0 or optimization_enabled: -
- ${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' - } - )} -
- - %else: -
- % endif - - - %if course_creator_status == "unrequested": -
-

- ${_('Becoming a Course Creator in {studio_name}').format(studio_name=settings.STUDIO_SHORT_NAME)} -

- -
-
-

${_('{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)}

-
- -
-

${_('Your Course Creator Request Status:')}

- -
-
- -
-
-
-
-
- - %elif course_creator_status == "denied": -
-

- ${_('Your Course Creator Request Status')} -

- -
-
-

${_('{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, - )}

-
- -
-

${_('Your Course Creator Request Status:')}

- -
-
${_('Your Course Creator request is:')}
-
- - ${_('Denied')} - ${_('Your request did not meet the criteria/guidelines specified by {platform_name} Staff.').format(platform_name=settings.PLATFORM_NAME)} -
-
-
-
-
- - %elif course_creator_status == "pending": -
-

- ${_('Your Course Creator Request Status')} -

- -
-
-

${_('{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, - )}

-
- -
-

${_('Your Course Creator Request Status:')}

- -
-
${_('Your Course Creator request is:')}
-
- - ${_('Pending')} - - ${_('Your request is currently being reviewed by {platform_name} staff and should be updated shortly.').format(platform_name=settings.PLATFORM_NAME)} - -
-
-
-
-
- % endif - - %if archived_courses: -
- % 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 -
- %endif - - %if len(libraries) > 0 or optimization_enabled: -
- ${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' - } - )} -
- - %else: -
-
-
-

${_("Were you expecting to see a particular library here?")}

-
-

${_('The library creator must give you access to the library. Contact the library creator or administrator for the library you are helping to author.')}

-
-
-
- % if show_new_library_button: -
-
-

${_('Create Your First Library')}

-
-

${_('Libraries hold a pool of components that can be re-used across multiple courses. Create your first library with the click of a button!')}

-
-
- - -
- % endif -
- %endif - -
- -
- - - % else: -
-
-
-

${_("Thanks for signing up, {name}!").format(name=user.username)}

-
- -
-
-

${_("We need to verify your email address")}

-
-

${_('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)}

-
-
-
-
- - -
- - %endif -
- diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index cc568299f40e..c5d75308eb6b 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -16,22 +16,12 @@