From 95e26ef6d3e61840fa1ad16bb0d2fd07a5815ad7 Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Fri, 5 Jun 2026 19:05:50 +0500 Subject: [PATCH 01/13] feat: remove legacy library user access, import, and export pages Co-Authored-By: Claude Sonnet 4.6 --- .../contentstore/views/import_export.py | 47 ++--- cms/djangoapps/contentstore/views/library.py | 44 +---- .../contentstore/views/tests/test_library.py | 29 +-- cms/static/cms/js/spec/main.js | 1 - cms/static/js/factories/manage_users_lib.js | 40 ---- .../js/spec/views/pages/library_users_spec.js | 172 ------------------ .../mock/mock-manage-users-lib-ro.underscore | 30 --- .../js/mock/mock-manage-users-lib.underscore | 60 ------ cms/templates/manage_users_lib.html | 122 ------------- cms/templates/widgets/header.html | 38 ---- cms/urls.py | 2 - 11 files changed, 18 insertions(+), 567 deletions(-) delete mode 100644 cms/static/js/factories/manage_users_lib.js delete mode 100644 cms/static/js/spec/views/pages/library_users_spec.js delete mode 100644 cms/templates/js/mock/mock-manage-users-lib-ro.underscore delete mode 100644 cms/templates/js/mock/mock-manage-users-lib.underscore delete mode 100644 cms/templates/manage_users_lib.html diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index ab0e1574515a..ec53e4d7a7aa 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -26,7 +26,6 @@ from django.views.decorators.http import require_GET, require_http_methods from edx_django_utils.monitoring import set_custom_attribute, set_custom_attributes_for_course_key from opaque_keys.edx.keys import CourseKey -from opaque_keys.edx.locator import LibraryLocator from openedx_authz.constants.permissions import COURSES_EXPORT_COURSE, COURSES_IMPORT_COURSE from path import Path as path from storages.backends.s3boto3 import S3Boto3Storage @@ -43,7 +42,8 @@ from ..storage import course_import_export_storage from ..tasks import CourseExportTask, CourseImportTask, export_olx, import_olx -from ..utils import IMPORTABLE_FILE_TYPES, get_export_url, get_import_url, reverse_course_url, reverse_library_url +from ..toggles import use_new_export_page, use_new_import_page +from ..utils import IMPORTABLE_FILE_TYPES, get_export_url, get_import_url, reverse_course_url __all__ = [ 'import_handler', 'import_status_handler', @@ -74,15 +74,9 @@ def import_handler(request, course_key_string): json: import a course via the .tar.gz or .zip file specified in request.FILES """ courselike_key = CourseKey.from_string(course_key_string) - library = isinstance(courselike_key, LibraryLocator) - if library: - successful_url = reverse_library_url('library_handler', courselike_key) - context_name = 'context_library' - courselike_block = modulestore().get_library(courselike_key) - else: - successful_url = reverse_course_url('course_handler', courselike_key) - context_name = 'context_course' - courselike_block = modulestore().get_course(courselike_key) + successful_url = reverse_course_url('course_handler', courselike_key) + context_name = 'context_course' + courselike_block = modulestore().get_course(courselike_key) if not user_has_course_permission( user=request.user, authz_permission=COURSES_IMPORT_COURSE.identifier, @@ -98,7 +92,7 @@ def import_handler(request, course_key_string): return _write_chunk(request, courselike_key) elif request.method == 'GET': # assume html - if not library: + if use_new_import_page(courselike_key): return redirect(get_import_url(courselike_key)) status_url = reverse_course_url( "import_status_handler", courselike_key, kwargs={'filename': "fillerName"} @@ -107,7 +101,7 @@ def import_handler(request, course_key_string): context_name: courselike_block, 'successful_import_redirect_url': successful_url, 'import_status_url': status_url, - 'library': isinstance(courselike_key, LibraryLocator) + 'library': False }) else: return HttpResponseNotFound() @@ -331,23 +325,14 @@ def export_handler(request, course_key_string): legacy_permission=LegacyAuthoringPermission.WRITE ): raise PermissionDenied() - library = isinstance(course_key, LibraryLocator) - if library: - courselike_block = modulestore().get_library(course_key) - context = { - 'context_library': courselike_block, - 'courselike_home_url': reverse_library_url("library_handler", course_key), - 'library': True - } - else: - courselike_block = modulestore().get_course(course_key) - if courselike_block is None: - raise Http404 - context = { - 'context_course': courselike_block, - 'courselike_home_url': reverse_course_url("course_handler", course_key), - 'library': False - } + courselike_block = modulestore().get_course(course_key) + if courselike_block is None: + raise Http404 + context = { + 'context_course': courselike_block, + 'courselike_home_url': reverse_course_url("course_handler", course_key), + 'library': False + } context['status_url'] = reverse_course_url('export_status_handler', course_key) # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header. @@ -357,7 +342,7 @@ def export_handler(request, course_key_string): export_olx.delay(request.user.id, course_key_string, request.LANGUAGE_CODE) return JsonResponse({'ExportStatus': 1}) elif 'text/html' in requested_format: - if not library: + if use_new_export_page(course_key): return redirect(get_export_url(course_key)) return render_to_response('export.html', context) else: diff --git a/cms/djangoapps/contentstore/views/library.py b/cms/djangoapps/contentstore/views/library.py index 70ae0bce2c2a..36371a47964c 100644 --- a/cms/djangoapps/contentstore/views/library.py +++ b/cms/djangoapps/contentstore/views/library.py @@ -24,16 +24,12 @@ from cms.djangoapps.course_creators.views import get_course_creator_status from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.student.auth import ( - STUDIO_EDIT_ROLES, - STUDIO_VIEW_USERS, - get_user_permissions, has_studio_read_access, has_studio_write_access, ) from common.djangoapps.student.roles import ( CourseInstructorRole, CourseStaffRole, - LibraryUserRole, OrgStaffRole, UserBasedRole, ) @@ -45,9 +41,8 @@ from ..toggles import libraries_v1_enabled from ..utils import add_instructor, reverse_library_url from .component import CONTAINER_TEMPLATES, get_component_templates -from .user import user_with_role -__all__ = ['library_handler', 'manage_library_users'] +__all__ = ['library_handler'] log = logging.getLogger(__name__) @@ -313,40 +308,3 @@ def library_blocks_view(library, user, response_format): }) -def manage_library_users(request, library_key_string): - """ - Studio UI for editing the users within a library. - - Uses the /course_team/:library_key/:user_email/ REST API to make changes. - """ - library_key = CourseKey.from_string(library_key_string) - if not isinstance(library_key, LibraryLocator): - raise Http404 # This is not a library - user_perms = get_user_permissions(request.user, library_key) - if not user_perms & STUDIO_VIEW_USERS: - raise PermissionDenied() - library = modulestore().get_library(library_key) - if library is None: - raise Http404 - - # Segment all the users explicitly associated with this library, ensuring each user only has one role listed: - instructors = set(CourseInstructorRole(library_key).users_with_role()) - staff = set(CourseStaffRole(library_key).users_with_role()) - instructors - users = set(LibraryUserRole(library_key).users_with_role()) - instructors - staff - - formatted_users = [] - for user in instructors: - formatted_users.append(user_with_role(user, 'instructor')) - for user in staff: - formatted_users.append(user_with_role(user, 'staff')) - for user in users: - formatted_users.append(user_with_role(user, 'library_user')) - - return render_to_response('manage_users_lib.html', { - 'context_library': library, - 'users': formatted_users, - 'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES), - 'library_key': str(library_key), - 'lib_users_url': reverse_library_url('manage_library_users', library_key_string), - 'show_children_previews': library.show_children_previews - }) diff --git a/cms/djangoapps/contentstore/views/tests/test_library.py b/cms/djangoapps/contentstore/views/tests/test_library.py index 172f87addb56..36d45553ab1c 100644 --- a/cms/djangoapps/contentstore/views/tests/test_library.py +++ b/cms/djangoapps/contentstore/views/tests/test_library.py @@ -21,7 +21,7 @@ from cms.djangoapps.course_creators.models import CourseCreator from cms.djangoapps.course_creators.views import add_user_with_status_granted as grant_course_creator_status from common.djangoapps.student import auth -from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, LibraryUserRole +from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole from xmodule.modulestore.tests.factories import LibraryFactory # pylint: disable=wrong-import-order from ..component import get_component_templates @@ -427,33 +427,6 @@ def test_advanced_problem_types(self): for advance_problem_type in settings.ADVANCED_PROBLEM_TYPES: self.assertNotIn(advance_problem_type['component'], problem_type_categories) # noqa: PT009 - def test_manage_library_users(self): - """ - Simple test that the Library "User Access" view works. - Also tests that we can use the REST API to assign a user to a library. - """ - library = LibraryFactory.create() - extra_user, _ = self.create_non_staff_user() - manage_users_url = reverse_library_url('manage_library_users', str(library.location.library_key)) - - response = self.client.get(manage_users_url) - self.assertEqual(response.status_code, 200) # noqa: PT009 - # extra_user has not been assigned to the library so should not show up in the list: - self.assertNotContains(response, extra_user.username) - - # Now add extra_user to the library: - user_details_url = reverse_course_url( - 'course_team_handler', - library.location.library_key, kwargs={'email': extra_user.email} - ) - edit_response = self.client.ajax_post(user_details_url, {"role": LibraryUserRole.ROLE}) - self.assertIn(edit_response.status_code, (200, 204)) # noqa: PT009 - - # Now extra_user should apear in the list: - response = self.client.get(manage_users_url) - self.assertEqual(response.status_code, 200) # noqa: PT009 - self.assertContains(response, extra_user.username) - def test_component_limits(self): """ Test that component limits in libraries are respected. diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index de76a6dda5a5..019fb2b67887 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -257,7 +257,6 @@ 'js/spec/views/paging_spec', 'js/spec/views/pages/course_rerun_spec', 'js/spec/views/pages/index_spec', - 'js/spec/views/pages/library_users_spec', 'js/spec/views/modals/base_modal_spec', 'js/spec/views/modals/move_xblock_modal_spec' ]; diff --git a/cms/static/js/factories/manage_users_lib.js b/cms/static/js/factories/manage_users_lib.js deleted file mode 100644 index 434eb3baa77f..000000000000 --- a/cms/static/js/factories/manage_users_lib.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - Code for editing users and assigning roles within a library context. -*/ -define(['underscore', 'gettext', 'js/views/manage_users_and_roles'], - function(_, gettext, ManageUsersAndRoles) { - 'use strict'; - - return function(containerName, users, tplUserURL, current_user_id, allow_actions) { - function updateMessages(messages) { - var local_messages = _.extend({}, messages); - local_messages.alreadyMember.title = gettext('Already a library team member'); - local_messages.deleteUser.messageTpl = gettext( - 'Are you sure you want to delete {email} from the library “{container}”?' - ); - return local_messages; - } - // Roles order are important: first role is considered initial role (the role added to user when (s)he's added - // Last role is considered an admin role (unrestricted access + ability to manage other users' permissions) - // Changing roles is performed in promote-demote fashion, so moves only to adjacent roles is allowed - var roles = [ - {key: 'library_user', name: gettext('Library User')}, - {key: 'staff', name: gettext('Staff')}, - {key: 'instructor', name: gettext('Admin')} - ]; - - var options = { - el: $('#content'), - containerName: containerName, - tplUserURL: tplUserURL, - roles: roles, - users: users, - messages_modifier: updateMessages, - current_user_id: current_user_id, - allow_actions: allow_actions - }; - - var view = new ManageUsersAndRoles(options); - view.render(); - }; - }); diff --git a/cms/static/js/spec/views/pages/library_users_spec.js b/cms/static/js/spec/views/pages/library_users_spec.js deleted file mode 100644 index 08dd6065eb9c..000000000000 --- a/cms/static/js/spec/views/pages/library_users_spec.js +++ /dev/null @@ -1,172 +0,0 @@ -define([ - 'jquery', 'sinon', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/spec_helpers/view_helpers', - 'js/factories/manage_users_lib', 'common/js/components/utils/view_utils' -], -function($, sinon, AjaxHelpers, ViewHelpers, ManageUsersFactory, ViewUtils) { - 'use strict'; - - var requests, xhrFactory; - - describe('Library Instructor Access Page', function() { - var changeRoleUrl = 'dummy_change_role_url/@@EMAIL@@'; - var team_member_fixture = readFixtures('team-member.underscore'); - - function setRole(email, role) { - var $user_li = $('li.user-item[data-email="' + email + '"]'); - var $role_action = $('li.action-role a.make-' + role, $user_li); - expect($role_action).toBeVisible(); - $role_action.click(); - } - - function getUrl(email) { - return changeRoleUrl.replace('@@EMAIL@@', email); - } - - describe('read-write access', function() { - var mockHTML = readFixtures('mock/mock-manage-users-lib.underscore'); - - beforeEach(function(done) { - xhrFactory = sinon.useFakeXMLHttpRequest(); - requests = []; - requests.currentIndex = 0; - requests.restore = function() { xhrFactory.restore(); }; - xhrFactory.onCreate = function(req) { requests.push(req); }; - ViewHelpers.installMockAnalytics(); - setFixtures(mockHTML); - appendSetFixtures($(' - - -<%block name="content"> - -
-
-

- ${_("Settings")} - > ${_("User Access")} -

- - -
-
- -
-
-
- %if allow_actions: -
-
-
-

${_("Grant Access to This Library")}

- -
- ${_("New Team Member Information")} - -
    -
  1. - - - ${_("Provide the email address of the user you want to add")} -
  2. -
-
-
- -
- - -
-
-
- %endif - -
    -
    -

    ${_("Loading")}

    -
    -
- - % if allow_actions and len(users) == 1: -
-
-

${_('Add More Users to This Library')}

-
-

${_('Grant other members of your course team access to this library. New library users must have an active {studio_name} account.').format(studio_name=settings.STUDIO_SHORT_NAME)}

-
-
- - -
- %endif -
- - -
-
- - -<%block name="requirejs"> - require(["js/factories/manage_users_lib"], function(ManageLibraryUsersFactory) { - ManageLibraryUsersFactory( - "${context_library.display_name_with_default | n, js_escaped_string}", - ${users | n, dump_js_escaped_json}, - "${reverse('course_team_handler', kwargs={'course_key_string': library_key, 'email': '@@EMAIL@@'}) | n, js_escaped_string}", - ${request.user.id | n, dump_js_escaped_json}, - ${allow_actions | n, dump_js_escaped_json} - ); - }); - diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index c67c91dd21b0..ad900a084933 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -174,9 +174,6 @@

${_("Tools")} ${_("Current Library:")} @@ -185,41 +182,6 @@

${context_library.display_name_with_default}

- - % endif diff --git a/cms/urls.py b/cms/urls.py index faa8281cf9d3..7256bf1c5606 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -237,8 +237,6 @@ urlpatterns += [ re_path(fr'^library/{LIBRARY_KEY_PATTERN}?$', contentstore_views.library_handler, name='library_handler'), - re_path(fr'^library/{LIBRARY_KEY_PATTERN}/team/$', - contentstore_views.manage_library_users, name='manage_library_users'), ] if toggles.EXPORT_GIT.is_enabled(): From 67aa1e928aa8a173d2846c779c062333525e135a Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Fri, 5 Jun 2026 19:39:58 +0500 Subject: [PATCH 02/13] fix: remove unused imports from test_library.py Co-Authored-By: Claude Sonnet 4.6 --- cms/djangoapps/contentstore/views/tests/test_library.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/tests/test_library.py b/cms/djangoapps/contentstore/views/tests/test_library.py index 36d45553ab1c..d1c04fec7df0 100644 --- a/cms/djangoapps/contentstore/views/tests/test_library.py +++ b/cms/djangoapps/contentstore/views/tests/test_library.py @@ -17,7 +17,6 @@ from organizations.exceptions import InvalidOrganizationException from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase, parse_json -from cms.djangoapps.contentstore.utils import reverse_course_url, reverse_library_url from cms.djangoapps.course_creators.models import CourseCreator from cms.djangoapps.course_creators.views import add_user_with_status_granted as grant_course_creator_status from common.djangoapps.student import auth From b01c7d765d89d6fa9bf62e59fa319ef3552e74ce Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Fri, 5 Jun 2026 19:52:41 +0500 Subject: [PATCH 03/13] fix: remove deleted toggle imports and dead code from import_export views Co-Authored-By: Claude Sonnet 4.6 --- .../contentstore/views/import_export.py | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index ec53e4d7a7aa..ae87218b1c99 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -32,7 +32,6 @@ from user_tasks.conf import settings as user_tasks_settings from user_tasks.models import UserTaskArtifact, UserTaskStatus -from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.util.json_request import JsonResponse from common.djangoapps.util.monitoring import monitor_import_failure from common.djangoapps.util.views import ensure_valid_course_key @@ -42,7 +41,6 @@ from ..storage import course_import_export_storage from ..tasks import CourseExportTask, CourseImportTask, export_olx, import_olx -from ..toggles import use_new_export_page, use_new_import_page from ..utils import IMPORTABLE_FILE_TYPES, get_export_url, get_import_url, reverse_course_url __all__ = [ @@ -74,9 +72,6 @@ def import_handler(request, course_key_string): json: import a course via the .tar.gz or .zip file specified in request.FILES """ courselike_key = CourseKey.from_string(course_key_string) - successful_url = reverse_course_url('course_handler', courselike_key) - context_name = 'context_course' - courselike_block = modulestore().get_course(courselike_key) if not user_has_course_permission( user=request.user, authz_permission=COURSES_IMPORT_COURSE.identifier, @@ -92,17 +87,7 @@ def import_handler(request, course_key_string): return _write_chunk(request, courselike_key) elif request.method == 'GET': # assume html - if use_new_import_page(courselike_key): - return redirect(get_import_url(courselike_key)) - status_url = reverse_course_url( - "import_status_handler", courselike_key, kwargs={'filename': "fillerName"} - ) - return render_to_response('import.html', { - context_name: courselike_block, - 'successful_import_redirect_url': successful_url, - 'import_status_url': status_url, - 'library': False - }) + return redirect(get_import_url(courselike_key)) else: return HttpResponseNotFound() @@ -325,15 +310,8 @@ def export_handler(request, course_key_string): legacy_permission=LegacyAuthoringPermission.WRITE ): raise PermissionDenied() - courselike_block = modulestore().get_course(course_key) - if courselike_block is None: + if modulestore().get_course(course_key) is None: raise Http404 - context = { - 'context_course': courselike_block, - 'courselike_home_url': reverse_course_url("course_handler", course_key), - 'library': False - } - context['status_url'] = reverse_course_url('export_status_handler', course_key) # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header. requested_format = request.GET.get('_accept', request.META.get('HTTP_ACCEPT', 'text/html')) @@ -342,9 +320,7 @@ def export_handler(request, course_key_string): export_olx.delay(request.user.id, course_key_string, request.LANGUAGE_CODE) return JsonResponse({'ExportStatus': 1}) elif 'text/html' in requested_format: - if use_new_export_page(course_key): - return redirect(get_export_url(course_key)) - return render_to_response('export.html', context) + return redirect(get_export_url(course_key)) else: # Only HTML request format is supported (no JSON). return HttpResponse(status=406) From c27edca0ca64937d571acfdda2109db9d823b186 Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Mon, 8 Jun 2026 16:06:19 +0500 Subject: [PATCH 04/13] fix: remove trailing newlines from library.py --- cms/djangoapps/contentstore/views/library.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cms/djangoapps/contentstore/views/library.py b/cms/djangoapps/contentstore/views/library.py index 36371a47964c..ecce6b1378ff 100644 --- a/cms/djangoapps/contentstore/views/library.py +++ b/cms/djangoapps/contentstore/views/library.py @@ -306,5 +306,3 @@ def library_blocks_view(library, user, response_format): 'xblock_info': xblock_info, 'templates': CONTAINER_TEMPLATES, }) - - From 4d0fc7221614839b9592e44d862cb93d379be5d9 Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Tue, 9 Jun 2026 14:28:47 +0500 Subject: [PATCH 05/13] chore: delete unused import.html and export.html templates These templates were only ever rendered by the library branch of import_handler and export_handler. Both handlers now unconditionally redirect to the MFE, so the templates are dead code. --- cms/templates/export.html | 278 -------------------------------------- cms/templates/import.html | 260 ----------------------------------- 2 files changed, 538 deletions(-) delete mode 100644 cms/templates/export.html delete mode 100644 cms/templates/import.html diff --git a/cms/templates/export.html b/cms/templates/export.html deleted file mode 100644 index 4ee5605ccaec..000000000000 --- a/cms/templates/export.html +++ /dev/null @@ -1,278 +0,0 @@ -<%page expression_filter="h"/> -<%inherit file="base.html" /> -<%def name="online_help_token()"> -<% -if library: - return "export_library" -else: - return "export_course" -%> - -<%namespace name='static' file='static_content.html'/> - -<%! - from django.utils.translation import gettext as _ - from openedx.core.djangolib.markup import HTML, Text - from openedx.core.djangolib.js_utils import ( - dump_js_escaped_json, js_escaped_string - ) -%> -<%block name="title"> -%if library: - ${_("Library Export")} -%else: - ${_("Course Export")} -%endif - -<%block name="bodyclass">is-signedin course tools view-export - -<%block name="requirejs"> - var courselikeHomeUrl = "${courselike_home_url | n, js_escaped_string}", - is_library = ${library | n, dump_js_escaped_json}, - statusUrl = "${status_url | n, js_escaped_string}"; - - require(["js/factories/export"], function(ExportFactory) { - ExportFactory(courselikeHomeUrl, is_library, statusUrl); - }); - - -<%block name="content"> -
-
-

- ${_("Tools")} - > - %if library: - ${_("Library Export")} - %else: - ${_("Course Export")} - %endif -

-
-
- -
-
-
- -
- %if library: -

${_("About Exporting Libraries")}

-
- ## Translators: ".tar.gz" is a file extension, and should not be translated -

${_("You can export libraries and edit them outside of {studio_name}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the library structure and content. You can also re-import libraries that you've exported.").format( - studio_name=settings.STUDIO_SHORT_NAME, - )}

-
- %else: -

${_("About Exporting Courses")}

-
- ## Translators: ".tar.gz" is a file extension, and should not be translated -

${_("You can export courses and edit them outside of {studio_name}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.").format( - studio_name=settings.STUDIO_SHORT_NAME - )}

-

${Text(_("{em_start}Caution:{em_end} When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.")).format( - em_start=HTML(''), - em_end=HTML("") - )}

-
- %endif -
- -
-

- %if library: - ${_("Export My Library Content")} - %else: - ${_("Export My Course Content")} - %endif

- - -
- - - - %if not library: -
-
-

- ${Text(_("Data {em_start}exported with{em_end} your course:")).format( - em_start=HTML(''), - em_end=HTML("") - )}

-
    -
  • ${_("Values from Advanced Settings, including MATLAB API keys and LTI passports")}
  • -
  • ${_("Course Content (all Sections, Sub-sections, and Units)")}
  • -
  • ${_("Course Structure")}
  • -
  • ${_("Individual Problems")}
  • -
  • ${_("Pages")}
  • -
  • ${_("Course Assets")}
  • -
  • ${_("Course Settings")}
  • -
-
- -
-

- ${Text(_("Data {em_start}not exported{em_end} with your course:")).format( - em_start=HTML(''), - em_end=HTML("") - )}

-
    -
  • ${_("User Data")}
  • -
  • ${_("Course Team Data")}
  • -
  • ${_("Forum/discussion Data")}
  • -
  • ${_("Certificates")}
  • -
-
-
- %endif -
- %if library: - - %else: - - %endif -
-
- diff --git a/cms/templates/import.html b/cms/templates/import.html deleted file mode 100644 index eb127eaccb2b..000000000000 --- a/cms/templates/import.html +++ /dev/null @@ -1,260 +0,0 @@ -<%page expression_filter="h"/> -<%inherit file="base.html" /> -<%def name="online_help_token()"> -<% -if library: - return "import_library" -else: - return "import_course" -%> - -<%namespace name='static' file='static_content.html'/> -<%! - from django.utils.translation import gettext as _ - from openedx.core.djangolib.js_utils import ( - dump_js_escaped_json, js_escaped_string - ) - from openedx.core.djangolib.markup import HTML, Text -%> -<%block name="title"> -%if library: - ${_("Library Import")} -%else: - ${_("Course Import")} -%endif - -<%block name="bodyclass">is-signedin course tools view-import - -<%block name="content"> -
-
-

- ${_("Tools")} - > - %if library: - ${_("Library Import")} - %else: - ${_("Course Import")} - %endif -

-
-
- -
-
-
-
- ## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated - ## Translators: ".zip" is a file extension, and files with that extension are called "zipped files": these terms should not be translated - %if library: -

${Text(_("Be sure you want to import a library before continuing. The contents of the imported library will replace the contents of the existing library. {em_start}You cannot undo a library import{em_end}. Before you proceed, we recommend that you export the current library, so that you have a backup copy of it.")).format(em_start=HTML(''), em_end=HTML(""))}

-

${_("The library that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a library.xml file. It may also contain other files.")}

-

${_("The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the Unpacking stage has completed. We recommend, however, that you don't make important changes to your library until the import operation has completed.")}

- %else: -

${Text(_("Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. {em_start}You cannot undo a course import{em_end}. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.")).format(em_start=HTML(''), em_end=HTML(""))}

-

${_("The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip) or .zip (that is a compressed file). This .tar.gz or .zip file must contain a course.xml file. It may also contain other files.")}

-

${_("The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the Unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed.")}

- %endif - -
- -
- - ## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated - ## Translators: ".zip" is a file extension, and files with that extension are called "zipped files": these terms should not be translated -

- %if library: - ${_("Select a .tar.gz File to Replace Your Library Content")} - %else: - ${_("Select a .tar.gz or .zip File to Replace Your Course Content")} - %endif -

- -

- - - - ${_("Choose a File to Import")} - - -
-

- ${_("File Chosen:")} - -

- - - - -
- - -
-
- - %if library: - - %else: - - %endif -
-
-<%static:webpack entry="Import"> - Import('${import_status_url | n, js_escaped_string}', ${library | n, dump_js_escaped_json}); - - - From b96fe3cd092c009fdb1282229f38891f2af8a798 Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Tue, 9 Jun 2026 14:31:33 +0500 Subject: [PATCH 06/13] Revert "chore: delete unused import.html and export.html templates" This reverts commit 4d0fc7221614839b9592e44d862cb93d379be5d9. --- cms/templates/export.html | 278 ++++++++++++++++++++++++++++++++++++++ cms/templates/import.html | 260 +++++++++++++++++++++++++++++++++++ 2 files changed, 538 insertions(+) create mode 100644 cms/templates/export.html create mode 100644 cms/templates/import.html diff --git a/cms/templates/export.html b/cms/templates/export.html new file mode 100644 index 000000000000..4ee5605ccaec --- /dev/null +++ b/cms/templates/export.html @@ -0,0 +1,278 @@ +<%page expression_filter="h"/> +<%inherit file="base.html" /> +<%def name="online_help_token()"> +<% +if library: + return "export_library" +else: + return "export_course" +%> + +<%namespace name='static' file='static_content.html'/> + +<%! + from django.utils.translation import gettext as _ + from openedx.core.djangolib.markup import HTML, Text + from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string + ) +%> +<%block name="title"> +%if library: + ${_("Library Export")} +%else: + ${_("Course Export")} +%endif + +<%block name="bodyclass">is-signedin course tools view-export + +<%block name="requirejs"> + var courselikeHomeUrl = "${courselike_home_url | n, js_escaped_string}", + is_library = ${library | n, dump_js_escaped_json}, + statusUrl = "${status_url | n, js_escaped_string}"; + + require(["js/factories/export"], function(ExportFactory) { + ExportFactory(courselikeHomeUrl, is_library, statusUrl); + }); + + +<%block name="content"> +
+
+

+ ${_("Tools")} + > + %if library: + ${_("Library Export")} + %else: + ${_("Course Export")} + %endif +

+
+
+ +
+
+
+ +
+ %if library: +

${_("About Exporting Libraries")}

+
+ ## Translators: ".tar.gz" is a file extension, and should not be translated +

${_("You can export libraries and edit them outside of {studio_name}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the library structure and content. You can also re-import libraries that you've exported.").format( + studio_name=settings.STUDIO_SHORT_NAME, + )}

+
+ %else: +

${_("About Exporting Courses")}

+
+ ## Translators: ".tar.gz" is a file extension, and should not be translated +

${_("You can export courses and edit them outside of {studio_name}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.").format( + studio_name=settings.STUDIO_SHORT_NAME + )}

+

${Text(_("{em_start}Caution:{em_end} When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.")).format( + em_start=HTML(''), + em_end=HTML("") + )}

+
+ %endif +
+ +
+

+ %if library: + ${_("Export My Library Content")} + %else: + ${_("Export My Course Content")} + %endif

+ + +
+ + + + %if not library: +
+
+

+ ${Text(_("Data {em_start}exported with{em_end} your course:")).format( + em_start=HTML(''), + em_end=HTML("") + )}

+
    +
  • ${_("Values from Advanced Settings, including MATLAB API keys and LTI passports")}
  • +
  • ${_("Course Content (all Sections, Sub-sections, and Units)")}
  • +
  • ${_("Course Structure")}
  • +
  • ${_("Individual Problems")}
  • +
  • ${_("Pages")}
  • +
  • ${_("Course Assets")}
  • +
  • ${_("Course Settings")}
  • +
+
+ +
+

+ ${Text(_("Data {em_start}not exported{em_end} with your course:")).format( + em_start=HTML(''), + em_end=HTML("") + )}

+
    +
  • ${_("User Data")}
  • +
  • ${_("Course Team Data")}
  • +
  • ${_("Forum/discussion Data")}
  • +
  • ${_("Certificates")}
  • +
+
+
+ %endif +
+ %if library: + + %else: + + %endif +
+
+ diff --git a/cms/templates/import.html b/cms/templates/import.html new file mode 100644 index 000000000000..eb127eaccb2b --- /dev/null +++ b/cms/templates/import.html @@ -0,0 +1,260 @@ +<%page expression_filter="h"/> +<%inherit file="base.html" /> +<%def name="online_help_token()"> +<% +if library: + return "import_library" +else: + return "import_course" +%> + +<%namespace name='static' file='static_content.html'/> +<%! + from django.utils.translation import gettext as _ + from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string + ) + from openedx.core.djangolib.markup import HTML, Text +%> +<%block name="title"> +%if library: + ${_("Library Import")} +%else: + ${_("Course Import")} +%endif + +<%block name="bodyclass">is-signedin course tools view-import + +<%block name="content"> +
+
+

+ ${_("Tools")} + > + %if library: + ${_("Library Import")} + %else: + ${_("Course Import")} + %endif +

+
+
+ +
+
+
+
+ ## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated + ## Translators: ".zip" is a file extension, and files with that extension are called "zipped files": these terms should not be translated + %if library: +

${Text(_("Be sure you want to import a library before continuing. The contents of the imported library will replace the contents of the existing library. {em_start}You cannot undo a library import{em_end}. Before you proceed, we recommend that you export the current library, so that you have a backup copy of it.")).format(em_start=HTML(''), em_end=HTML(""))}

+

${_("The library that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a library.xml file. It may also contain other files.")}

+

${_("The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the Unpacking stage has completed. We recommend, however, that you don't make important changes to your library until the import operation has completed.")}

+ %else: +

${Text(_("Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. {em_start}You cannot undo a course import{em_end}. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.")).format(em_start=HTML(''), em_end=HTML(""))}

+

${_("The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip) or .zip (that is a compressed file). This .tar.gz or .zip file must contain a course.xml file. It may also contain other files.")}

+

${_("The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the Unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed.")}

+ %endif + +
+ +
+ + ## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated + ## Translators: ".zip" is a file extension, and files with that extension are called "zipped files": these terms should not be translated +

+ %if library: + ${_("Select a .tar.gz File to Replace Your Library Content")} + %else: + ${_("Select a .tar.gz or .zip File to Replace Your Course Content")} + %endif +

+ +

+ + + + ${_("Choose a File to Import")} + + +
+

+ ${_("File Chosen:")} + +

+ + + + +
+ + +
+
+ + %if library: + + %else: + + %endif +
+
+<%static:webpack entry="Import"> + Import('${import_status_url | n, js_escaped_string}', ${library | n, dump_js_escaped_json}); + + + From e18d519a2d12470dddcb4eb722088117dfeb78c6 Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Tue, 9 Jun 2026 14:36:54 +0500 Subject: [PATCH 07/13] chore: remove library branches from import.html and export.html templates --- cms/templates/export.html | 102 ++++++++------------------------------ cms/templates/import.html | 88 +++++--------------------------- 2 files changed, 32 insertions(+), 158 deletions(-) diff --git a/cms/templates/export.html b/cms/templates/export.html index 4ee5605ccaec..6f5ceb68b47a 100644 --- a/cms/templates/export.html +++ b/cms/templates/export.html @@ -2,10 +2,7 @@ <%inherit file="base.html" /> <%def name="online_help_token()"> <% -if library: - return "export_library" -else: - return "export_course" +return "export_course" %> <%namespace name='static' file='static_content.html'/> @@ -18,21 +15,16 @@ ) %> <%block name="title"> -%if library: - ${_("Library Export")} -%else: ${_("Course Export")} -%endif <%block name="bodyclass">is-signedin course tools view-export <%block name="requirejs"> var courselikeHomeUrl = "${courselike_home_url | n, js_escaped_string}", - is_library = ${library | n, dump_js_escaped_json}, statusUrl = "${status_url | n, js_escaped_string}"; require(["js/factories/export"], function(ExportFactory) { - ExportFactory(courselikeHomeUrl, is_library, statusUrl); + ExportFactory(courselikeHomeUrl, false, statusUrl); }); @@ -42,11 +34,7 @@

${_("Tools")} > - %if library: - ${_("Library Export")} - %else: ${_("Course Export")} - %endif

@@ -56,47 +44,31 @@

- %if library: -

${_("About Exporting Libraries")}

-
- ## Translators: ".tar.gz" is a file extension, and should not be translated -

${_("You can export libraries and edit them outside of {studio_name}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the library structure and content. You can also re-import libraries that you've exported.").format( - studio_name=settings.STUDIO_SHORT_NAME, - )}

-
- %else: -

${_("About Exporting Courses")}

-
- ## Translators: ".tar.gz" is a file extension, and should not be translated -

${_("You can export courses and edit them outside of {studio_name}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.").format( - studio_name=settings.STUDIO_SHORT_NAME - )}

-

${Text(_("{em_start}Caution:{em_end} When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.")).format( - em_start=HTML(''), - em_end=HTML("") - )}

-
- %endif +

${_("About Exporting Courses")}

+
+ ## Translators: ".tar.gz" is a file extension, and should not be translated +

${_("You can export courses and edit them outside of {studio_name}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.").format( + studio_name=settings.STUDIO_SHORT_NAME + )}

+

${Text(_("{em_start}Caution:{em_end} When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.")).format( + em_start=HTML(''), + em_end=HTML("") + )}

+

- %if library: - ${_("Export My Library Content")} - %else: - ${_("Export My Course Content")} - %endif

+ ${_("Export My Course Content")} +

@@ -104,11 +76,7 @@

- %if not library:

@@ -218,27 +177,7 @@

- %endif - %if library: - - %else: - %endif diff --git a/cms/templates/import.html b/cms/templates/import.html index eb127eaccb2b..edbee010f121 100644 --- a/cms/templates/import.html +++ b/cms/templates/import.html @@ -2,10 +2,7 @@ <%inherit file="base.html" /> <%def name="online_help_token()"> <% -if library: - return "import_library" -else: - return "import_course" +return "import_course" %> <%namespace name='static' file='static_content.html'/> @@ -17,11 +14,7 @@ from openedx.core.djangolib.markup import HTML, Text %> <%block name="title"> -%if library: - ${_("Library Import")} -%else: ${_("Course Import")} -%endif <%block name="bodyclass">is-signedin course tools view-import @@ -31,11 +24,7 @@

${_("Tools")} > - %if library: - ${_("Library Import")} - %else: - ${_("Course Import")} - %endif + ${_("Course Import")}

@@ -46,16 +35,9 @@

## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated ## Translators: ".zip" is a file extension, and files with that extension are called "zipped files": these terms should not be translated - %if library: -

${Text(_("Be sure you want to import a library before continuing. The contents of the imported library will replace the contents of the existing library. {em_start}You cannot undo a library import{em_end}. Before you proceed, we recommend that you export the current library, so that you have a backup copy of it.")).format(em_start=HTML(''), em_end=HTML(""))}

-

${_("The library that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a library.xml file. It may also contain other files.")}

-

${_("The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the Unpacking stage has completed. We recommend, however, that you don't make important changes to your library until the import operation has completed.")}

- %else: -

${Text(_("Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. {em_start}You cannot undo a course import{em_end}. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.")).format(em_start=HTML(''), em_end=HTML(""))}

-

${_("The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip) or .zip (that is a compressed file). This .tar.gz or .zip file must contain a course.xml file. It may also contain other files.")}

-

${_("The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the Unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed.")}

- %endif - +

${Text(_("Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. {em_start}You cannot undo a course import{em_end}. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.")).format(em_start=HTML(''), em_end=HTML(""))}

+

${_("The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip) or .zip (that is a compressed file). This .tar.gz or .zip file must contain a course.xml file. It may also contain other files.")}

+

${_("The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the Unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed.")}

@@ -63,11 +45,7 @@

## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated ## Translators: ".zip" is a file extension, and files with that extension are called "zipped files": these terms should not be translated

- %if library: - ${_("Select a .tar.gz File to Replace Your Library Content")} - %else: - ${_("Select a .tar.gz or .zip File to Replace Your Course Content")} - %endif + ${_("Select a .tar.gz or .zip File to Replace Your Course Content")}

@@ -86,21 +64,13 @@

<%static:webpack entry="Import"> - Import('${import_status_url | n, js_escaped_string}', ${library | n, dump_js_escaped_json}); + Import('${import_status_url | n, js_escaped_string}', false); - From 5211444f3182e96f10f8a5e5d92e9c24da89be21 Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Tue, 9 Jun 2026 16:52:50 +0500 Subject: [PATCH 08/13] chore: remove library branch from import JS factory and template --- cms/static/js/features/import/factories/import.js | 8 ++------ cms/templates/import.html | 6 ++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/cms/static/js/features/import/factories/import.js b/cms/static/js/features/import/factories/import.js index d9fe7b06fab4..ce12e7b1fe10 100644 --- a/cms/static/js/features/import/factories/import.js +++ b/cms/static/js/features/import/factories/import.js @@ -12,7 +12,7 @@ define([ const IMPORTABLE_FILE_TYPES = /\.tar\.gz$|\.zip$/; return { - Import: function(feedbackUrl, library) { + Import: function(feedbackUrl) { var dbError, $bar = $('.progress-bar'), $fill = $('.progress-fill'), @@ -52,11 +52,7 @@ define([ } }; - if (library) { - dbError = gettext('There was an error while importing the new library to our database.'); - } else { - dbError = gettext('There was an error while importing the new course to our database.'); - } + dbError = gettext('There was an error while importing the new course to our database.'); $(window).on('beforeunload', function() { unloading = true; }); diff --git a/cms/templates/import.html b/cms/templates/import.html index edbee010f121..a711a845c4c0 100644 --- a/cms/templates/import.html +++ b/cms/templates/import.html @@ -8,9 +8,7 @@ <%namespace name='static' file='static_content.html'/> <%! from django.utils.translation import gettext as _ - from openedx.core.djangolib.js_utils import ( - dump_js_escaped_json, js_escaped_string - ) + from openedx.core.djangolib.js_utils import js_escaped_string from openedx.core.djangolib.markup import HTML, Text %> <%block name="title"> @@ -191,6 +189,6 @@

${_("Warning: Importing while a course is running")}

<%static:webpack entry="Import"> - Import('${import_status_url | n, js_escaped_string}', false); + Import('${import_status_url | n, js_escaped_string}'); From df8968ce8bbd530a042b856e7aa29912336c010a Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Tue, 9 Jun 2026 17:00:08 +0500 Subject: [PATCH 09/13] chore: remove library branches from import/export JS and templates --- cms/static/js/factories/export.js | 8 ++++---- cms/static/js/views/export.js | 22 ++++++---------------- cms/templates/export.html | 6 ++---- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/cms/static/js/factories/export.js b/cms/static/js/factories/export.js index b2e8132064f8..a3ada7502360 100644 --- a/cms/static/js/factories/export.js +++ b/cms/static/js/factories/export.js @@ -3,7 +3,7 @@ define([ ], function(domReady, Export, $, gettext) { 'use strict'; - return function(courselikeHomeUrl, library, statusUrl) { + return function(courselikeHomeUrl, statusUrl) { var $submitBtn = $('.action-export'), unloading = false, previousExport = Export.storedExport(courselikeHomeUrl); @@ -15,7 +15,7 @@ define([ var startExport = function(e) { e.preventDefault(); $submitBtn.hide(); - Export.reset(library); + Export.reset(); Export.start(statusUrl).then(onComplete); $.ajax({ type: 'POST', @@ -30,7 +30,7 @@ define([ if (!unloading) { $(window).off('beforeunload.import'); - Export.reset(library); + Export.reset(); onComplete(); Export.showError(gettext('Your export has failed.')); @@ -47,7 +47,7 @@ define([ if (previousExport.completed !== true) { $submitBtn.hide(); } - Export.resume(library).then(onComplete); + Export.resume().then(onComplete); } domReady(function() { diff --git a/cms/static/js/views/export.js b/cms/static/js/views/export.js index 18f1cc182a61..5a3b8e66eb97 100644 --- a/cms/static/js/views/export.js +++ b/cms/static/js/views/export.js @@ -28,7 +28,6 @@ define([ var courselikeHomeUrl; var current = {stage: 0, state: STATE.READY, downloadUrl: null}; var deferred = null; - var isLibrary = false; var statusUrl = null; var successUnixDate = null; var timeout = {id: null, delay: 1000}; @@ -264,11 +263,10 @@ define([ * Resets the Export internally and visually * */ - reset: function(library) { + reset: function() { current.stage = STAGE.PREPARING; current.state = STATE.READY; current.downloadUrl = null; - isLibrary = library; clearTimeout(timeout.id); updateFeedbackList(); @@ -280,9 +278,8 @@ define([ * * @return {jQuery promise} */ - resume: function(library) { + resume: function() { deferred = $.Deferred(); - isLibrary = library; statusUrl = this.storedExport().statusUrl; $.getJSON(statusUrl, function(data) { @@ -332,17 +329,10 @@ define([ } }); } else { - if (isLibrary) { - msg += gettext('Your library could not be exported to XML. There is not enough information to ' - + 'identify the failed component. Inspect your library to identify any problematic components ' - + 'and try again.'); - action = gettext('Take me to the main library page'); - } else { - msg += gettext('Your course could not be exported to XML. There is not enough information to ' - + 'identify the failed component. Inspect your course to identify any problematic components ' - + 'and try again.'); - action = gettext('Take me to the main course page'); - } + msg += gettext('Your course could not be exported to XML. There is not enough information to ' + + 'identify the failed component. Inspect your course to identify any problematic components ' + + 'and try again.'); + action = gettext('Take me to the main course page'); msg += ' ' + gettext('The raw error message is:') + ' ' + errMsg; dialog = new PromptView({ title: gettext('There has been an error with your export.'), diff --git a/cms/templates/export.html b/cms/templates/export.html index 6f5ceb68b47a..44922b5eedb7 100644 --- a/cms/templates/export.html +++ b/cms/templates/export.html @@ -10,9 +10,7 @@ <%! from django.utils.translation import gettext as _ from openedx.core.djangolib.markup import HTML, Text - from openedx.core.djangolib.js_utils import ( - dump_js_escaped_json, js_escaped_string - ) + from openedx.core.djangolib.js_utils import js_escaped_string %> <%block name="title"> ${_("Course Export")} @@ -24,7 +22,7 @@ statusUrl = "${status_url | n, js_escaped_string}"; require(["js/factories/export"], function(ExportFactory) { - ExportFactory(courselikeHomeUrl, false, statusUrl); + ExportFactory(courselikeHomeUrl, statusUrl); }); From e25edc0a87788e3c8e61be879eb62d05be4dc5e7 Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Wed, 10 Jun 2026 01:05:28 +0500 Subject: [PATCH 10/13] chore: remove library-specific role handling from course team REST API --- cms/djangoapps/contentstore/views/user.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index a5e926e7fefe..21f8680440c2 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -9,13 +9,12 @@ from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_http_methods, require_POST from opaque_keys.edx.keys import CourseKey -from opaque_keys.edx.locator import LibraryLocator from cms.djangoapps.course_creators.views import user_requested_access from common.djangoapps.student import auth from common.djangoapps.student.auth import STUDIO_EDIT_ROLES, STUDIO_VIEW_USERS, get_user_permissions from common.djangoapps.student.models import CourseEnrollment -from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, LibraryUserRole +from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole from common.djangoapps.util.json_request import JsonResponse, expect_json from ..utils import get_course_team_url @@ -95,12 +94,8 @@ def _course_team_user(request, course_key, email): } return JsonResponse(msg, 404) - is_library = isinstance(course_key, LibraryLocator) # Ordered list of roles: can always move self to the right, but need STUDIO_EDIT_ROLES to move any user left - if is_library: - role_hierarchy = (CourseInstructorRole, CourseStaffRole, LibraryUserRole) - else: - role_hierarchy = (CourseInstructorRole, CourseStaffRole) + role_hierarchy = (CourseInstructorRole, CourseStaffRole) if request.method == "GET": # just return info about the user @@ -164,7 +159,7 @@ def _course_team_user(request, course_key, email): return JsonResponse(msg, 400) auth.remove_users(request.user, role, user) - if new_role and not is_library: + if new_role: # The user may be newly added to this course. # auto-enroll the user in the course so that "View Live" will work. CourseEnrollment.enroll(user, course_key) From 8ef839acdaf396795eb0e611eaa2c49303d49dce Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Wed, 17 Jun 2026 00:38:38 +0500 Subject: [PATCH 11/13] chore: reject legacy library keys in import_handler with 404 Adds an early LibraryLocator check in import_handler to prevent a silent redirect to an invalid MFE URL when a legacy v1 library key is passed. Co-Authored-By: Claude Sonnet 4.6 --- cms/djangoapps/contentstore/views/import_export.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index ae87218b1c99..7402b15cadf2 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -26,6 +26,7 @@ from django.views.decorators.http import require_GET, require_http_methods from edx_django_utils.monitoring import set_custom_attribute, set_custom_attributes_for_course_key from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.locator import LibraryLocator from openedx_authz.constants.permissions import COURSES_EXPORT_COURSE, COURSES_IMPORT_COURSE from path import Path as path from storages.backends.s3boto3 import S3Boto3Storage @@ -72,6 +73,10 @@ def import_handler(request, course_key_string): json: import a course via the .tar.gz or .zip file specified in request.FILES """ courselike_key = CourseKey.from_string(course_key_string) + # Legacy (v1) libraries are no longer supported for import. Reject early + # to avoid an invalid redirect to the MFE with a library key. + if isinstance(courselike_key, LibraryLocator): + raise Http404 if not user_has_course_permission( user=request.user, authz_permission=COURSES_IMPORT_COURSE.identifier, From 8992b61ab17f5a60efbdd8af01ff2d1f85771557 Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Thu, 18 Jun 2026 15:32:12 +0500 Subject: [PATCH 12/13] fix: initialize dbError before defaults array is constructed dbError was declared but unassigned when defaults was built, causing defaults[3] to be 'undefined\n' instead of the correct error message. Co-Authored-By: Claude Sonnet 4.6 --- cms/static/js/features/import/factories/import.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cms/static/js/features/import/factories/import.js b/cms/static/js/features/import/factories/import.js index ce12e7b1fe10..78ad40261a2a 100644 --- a/cms/static/js/features/import/factories/import.js +++ b/cms/static/js/features/import/factories/import.js @@ -13,7 +13,7 @@ define([ return { Import: function(feedbackUrl) { - var dbError, + var dbError = gettext('There was an error while importing the new course to our database.'), $bar = $('.progress-bar'), $fill = $('.progress-fill'), $submitBtn = $('.submit-button'), @@ -52,8 +52,6 @@ define([ } }; - dbError = gettext('There was an error while importing the new course to our database.'); - $(window).on('beforeunload', function() { unloading = true; }); // Display the status of last file upload on page load From d9c066fb8e7a6714e52cac583c74fef8752cfc12 Mon Sep 17 00:00:00 2001 From: salmannawaz Date: Thu, 18 Jun 2026 15:40:49 +0500 Subject: [PATCH 13/13] test: lock in 404 behavior for legacy library keys in import/export handlers Co-Authored-By: Claude Sonnet 4.6 --- .../views/tests/test_import_export.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py index 260992343fdd..8caeb2e84ec1 100644 --- a/cms/djangoapps/contentstore/views/tests/test_import_export.py +++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py @@ -723,6 +723,17 @@ def test_import_failure_for_unknown_failures(self, exception, mocked_import, moc status_response = self.get_import_status(self.course.id, good_file) self.assertImportStatusResponse(status_response, self.UpdatingError, import_error.UNKNOWN_ERROR_IN_IMPORT) + def test_import_handler_rejects_legacy_library_key(self): + """ + Passing a legacy (v1) LibraryLocator key to import_handler must return 404. + This guards against the early-reject added in import_handler from being + accidentally removed. + """ + lib_key = LibraryLocator(org='TestOrg', library='TestLib') + url = reverse_course_url('import_handler', lib_key) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 404) # noqa: PT009 + @ddt.data('zip', 'tar') def test_import_status_response_is_not_cached(self, fmt): """To test import_status endpoint response is not cached""" @@ -945,6 +956,16 @@ def test_export_course_does_not_exist(self, url): resp = self.client.get_html(url) self.assertEqual(resp.status_code, 404) # noqa: PT009 + def test_export_handler_rejects_legacy_library_key(self): + """ + Passing a legacy (v1) LibraryLocator key to export_handler must return 404 + because modulestore().get_course() returns None for a library key. + """ + lib_key = LibraryLocator(org='TestOrg', library='TestLib') + url = reverse_course_url('export_handler', lib_key) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 404) # noqa: PT009 + def test_non_course_author(self): """ Verify that users who aren't authors of the course are unable to export it