"
- EDITABLE_DATE_FIELD = "
"
- EDITABLE_TIME_FIELD = "
"
-
- EDITABLE_ELEMENTS = [
- EDITABLE_DATE_WRAPPER,
- EDITABLE_TIME_WRAPPER,
- EDITABLE_DATE_FIELD,
- EDITABLE_TIME_FIELD,
- ]
-
- NOT_EDITABLE_ELEMENTS = [
- NOT_EDITABLE_HELPER_MESSAGE,
- NOT_EDITABLE_DATE_WRAPPER,
- NOT_EDITABLE_TIME_WRAPPER,
- NOT_EDITABLE_DATE_FIELD,
- NOT_EDITABLE_TIME_FIELD,
- ]
-
- def setUp(self):
- """
- Initialize course used to test enrollment fields.
- """
- super().setUp()
- self.course = CourseFactory.create(org='edX', number='dummy', display_name='Marketing Site Course')
- self.course_details_url = reverse_course_url('settings_handler', str(self.course.id))
-
- def _get_course_details_response(self, global_staff):
- """
- Return the course details page as either global or non-global staff
- """
- user = UserFactory(is_staff=global_staff, password=self.TEST_PASSWORD)
- CourseInstructorRole(self.course.id).add_users(user)
-
- self.client.login(username=user.username, password=self.TEST_PASSWORD)
-
- return self.client.get_html(self.course_details_url)
-
- def _verify_editable(self, response):
- """
- Verify that the response has expected editable fields.
-
- Assert that all editable field content exists and no
- uneditable field content exists for enrollment end fields.
- """
- self.assertEqual(response.status_code, 200) # noqa: PT009
- for element in self.NOT_EDITABLE_ELEMENTS:
- self.assertNotContains(response, element)
-
- for element in self.EDITABLE_ELEMENTS:
- self.assertContains(response, element)
-
- def _verify_not_editable(self, response):
- """
- Verify that the response has expected non-editable fields.
-
- Assert that all uneditable field content exists and no
- editable field content exists for enrollment end fields.
- """
- self.assertEqual(response.status_code, 200) # noqa: PT009
- for element in self.NOT_EDITABLE_ELEMENTS:
- self.assertContains(response, element)
-
- for element in self.EDITABLE_ELEMENTS:
- self.assertNotContains(response, element)
-
- @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': False})
- @override_waffle_flag(toggles.LEGACY_STUDIO_SCHEDULE_DETAILS, True)
- def test_course_details_with_disabled_setting_global_staff(self):
- """
- Test that user enrollment end date is editable in response.
-
- Feature flag 'ENABLE_PUBLISHER' is not enabled.
- User is global staff.
- """
- self._verify_editable(self._get_course_details_response(True))
-
- @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': False})
- @override_waffle_flag(toggles.LEGACY_STUDIO_SCHEDULE_DETAILS, True)
- def test_course_details_with_disabled_setting_non_global_staff(self):
- """
- Test that user enrollment end date is editable in response.
-
- Feature flag 'ENABLE_PUBLISHER' is not enabled.
- User is non-global staff.
- """
- self._verify_editable(self._get_course_details_response(False))
-
- @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': True})
- @override_waffle_flag(toggles.LEGACY_STUDIO_SCHEDULE_DETAILS, True)
- def test_course_details_with_enabled_setting_global_staff(self):
- """
- Test that user enrollment end date is editable in response.
-
- Feature flag 'ENABLE_PUBLISHER' is enabled.
- User is global staff.
- """
- self._verify_editable(self._get_course_details_response(True))
-
- @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': True})
- @override_settings(PLATFORM_NAME='edX')
- @override_waffle_flag(toggles.LEGACY_STUDIO_SCHEDULE_DETAILS, True)
- def test_course_details_with_enabled_setting_non_global_staff(self):
- """
- Test that user enrollment end date is not editable in response.
-
- Feature flag 'ENABLE_PUBLISHER' is enabled.
- User is non-global staff.
- """
- self._verify_not_editable(self._get_course_details_response(False))
diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py
index f4e513ce6709..32640abe2ca8 100644
--- a/cms/djangoapps/contentstore/toggles.py
+++ b/cms/djangoapps/contentstore/toggles.py
@@ -159,25 +159,6 @@ def use_react_markdown_editor(course_key):
return ENABLE_REACT_MARKDOWN_EDITOR.is_enabled(course_key)
-# .. toggle_name: legacy_studio.schedule_details
-# .. toggle_implementation: WaffleFlag
-# .. toggle_default: False
-# .. toggle_description: Temporarily fall back to the old Studio Schedule & Details 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_SCHEDULE_DETAILS = CourseWaffleFlag('legacy_studio.schedule_details', __name__)
-
-
-def use_new_schedule_details_page(course_key):
- """
- Returns a boolean if new studio schedule and details mfe is enabled
- """
- return not LEGACY_STUDIO_SCHEDULE_DETAILS.is_enabled(course_key)
-
-
# .. toggle_name: legacy_studio.advanced_settings
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 63c97a855c32..e5142f8da263 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -47,7 +47,6 @@
use_new_export_page,
use_new_group_configurations_page,
use_new_import_page,
- use_new_schedule_details_page,
use_new_unit_page,
)
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
@@ -303,13 +302,10 @@ def get_schedule_details_url(course_locator) -> str:
"""
Gets course authoring microfrontend URL for schedule and details pages view.
"""
- schedule_details_url = None
- if use_new_schedule_details_page(course_locator):
- mfe_base_url = get_course_authoring_url(course_locator)
- course_mfe_url = f'{mfe_base_url}/course/{course_locator}/settings/details'
- if mfe_base_url:
- schedule_details_url = course_mfe_url
- return schedule_details_url
+ mfe_base_url = get_course_authoring_url(course_locator)
+ if mfe_base_url:
+ return f'{mfe_base_url}/course/{course_locator}/settings/details'
+ return None
def get_advanced_settings_url(course_locator) -> str:
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index ee9ba6cddb4f..0762d093d53c 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -101,14 +101,12 @@
default_enable_flexible_peer_openassessments,
use_new_advanced_settings_page,
use_new_group_configurations_page,
- use_new_schedule_details_page,
)
from ..utils import (
add_instructor,
get_advanced_settings_url,
get_course_outline_url,
get_course_rerun_context,
- get_course_settings,
get_grading_url,
get_group_configurations_context,
get_group_configurations_url,
@@ -1380,10 +1378,7 @@ def settings_handler(request, course_key_string): # pylint: disable=too-many-st
with modulestore().bulk_operations(course_key):
course_block = get_course_and_check_access(course_key, request.user)
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
- if use_new_schedule_details_page(course_key):
- return redirect(get_schedule_details_url(course_key))
- settings_context = get_course_settings(request, course_key, course_block)
- return render_to_response('settings.html', settings_context)
+ return redirect(get_schedule_details_url(course_key))
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): # pylint: disable=too-many-nested-blocks
if request.method == 'GET':
course_details = CourseDetails.fetch(course_key)
diff --git a/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py b/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py
index f924b26384e7..686a3ab55267 100644
--- a/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py
+++ b/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py
@@ -3,17 +3,9 @@
"""
-from unittest import mock
-
-from edx_toggles.toggles.testutils import override_waffle_flag
-
-from cms.djangoapps.contentstore import toggles
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.utils import reverse_course_url
-from openedx.core.djangoapps.credit.api import get_credit_requirements
-from openedx.core.djangoapps.credit.models import CreditCourse
-from openedx.core.djangoapps.credit.signals.handlers import on_course_publish
-from xmodule.modulestore.tests.factories import CourseFactory # pylint: disable=wrong-import-order
+from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
class CreditEligibilityTest(CourseTestCase):
@@ -25,43 +17,3 @@ def setUp(self):
super().setUp()
self.course = CourseFactory.create(org='edX', number='dummy', display_name='Credit Course')
self.course_details_url = reverse_course_url('settings_handler', str(self.course.id))
-
- @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_CREDIT_ELIGIBILITY': False})
- @override_waffle_flag(toggles.LEGACY_STUDIO_SCHEDULE_DETAILS, True)
- def test_course_details_with_disabled_setting(self):
- """
- Test that user don't see credit eligibility requirements in response
- if the feature flag 'ENABLE_CREDIT_ELIGIBILITY' is not enabled.
- """
- response = self.client.get_html(self.course_details_url)
- self.assertEqual(response.status_code, 200) # noqa: PT009
- self.assertNotContains(response, "Course Credit Requirements")
- self.assertNotContains(response, "Steps required to earn course credit")
-
- @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_CREDIT_ELIGIBILITY': True})
- @override_waffle_flag(toggles.LEGACY_STUDIO_SCHEDULE_DETAILS, True)
- def test_course_details_with_enabled_setting(self):
- """
- Test that credit eligibility requirements are present in
- response if the feature flag 'ENABLE_CREDIT_ELIGIBILITY' is enabled.
- """
- # verify that credit eligibility requirements block don't show if the
- # course is not set as credit course
- response = self.client.get_html(self.course_details_url)
- self.assertEqual(response.status_code, 200) # noqa: PT009
- self.assertNotContains(response, "Course Credit Requirements")
- self.assertNotContains(response, "Steps required to earn course credit")
-
- # verify that credit eligibility requirements block shows if the
- # course is set as credit course and it has eligibility requirements
- credit_course = CreditCourse(course_key=str(self.course.id), enabled=True)
- credit_course.save()
- self.assertEqual(len(get_credit_requirements(self.course.id)), 0) # noqa: PT009
- # test that after publishing course, minimum grade requirement is added
- on_course_publish(self.course.id)
- self.assertEqual(len(get_credit_requirements(self.course.id)), 1) # noqa: PT009
-
- response = self.client.get_html(self.course_details_url)
- self.assertEqual(response.status_code, 200) # noqa: PT009
- self.assertContains(response, "Course Credit Requirements")
- self.assertContains(response, "Steps required to earn course credit")
diff --git a/cms/djangoapps/contentstore/views/tests/test_exam_settings_view.py b/cms/djangoapps/contentstore/views/tests/test_exam_settings_view.py
index f7edd959a2af..e59946d60d61 100644
--- a/cms/djangoapps/contentstore/views/tests/test_exam_settings_view.py
+++ b/cms/djangoapps/contentstore/views/tests/test_exam_settings_view.py
@@ -25,7 +25,6 @@
},
)
@override_waffle_flag(toggles.LEGACY_STUDIO_CERTIFICATES, True)
-@override_waffle_flag(toggles.LEGACY_STUDIO_SCHEDULE_DETAILS, True)
@override_waffle_flag(toggles.LEGACY_STUDIO_CONFIGURATIONS, True)
@override_waffle_flag(toggles.LEGACY_STUDIO_ADVANCED_SETTINGS, True)
@override_settings(COURSE_AUTHORING_MICROFRONTEND_URL='https://mfe.example')
@@ -52,7 +51,6 @@ def _get_exam_settings_alert_text(raw_html_content):
@override_waffle_flag(toggles.LEGACY_STUDIO_EXAM_SETTINGS, True)
@ddt.data(
"certificates_list_handler",
- "settings_handler",
"group_configurations_list_handler",
"advanced_settings_handler"
)
@@ -68,7 +66,6 @@ def test_view_without_exam_settings_enabled(self, handler):
@ddt.data(
"certificates_list_handler",
- "settings_handler",
"group_configurations_list_handler",
"advanced_settings_handler"
)
@@ -88,6 +85,13 @@ def test_grading_handler_redirects_to_mfe(self):
resp = self.client.get(url, HTTP_ACCEPT='text/html')
self.assertEqual(resp.status_code, 302) # noqa: PT009
+ def test_settings_handler_redirects_to_mfe(self):
+ """settings_handler (schedule & details) redirects to the authoring MFE."""
+ url = reverse_course_url('settings_handler', self.course.id)
+ resp = self.client.get(url, HTTP_ACCEPT='text/html')
+ self.assertEqual(resp.status_code, 302) # noqa: PT009
+
+
@override_settings(
PROCTORING_BACKENDS={
'DEFAULT': 'test_proctoring_provider',
diff --git a/cms/static/cms/js/build.js b/cms/static/cms/js/build.js
index f9f9bccea342..f544a38fec57 100644
--- a/cms/static/cms/js/build.js
+++ b/cms/static/cms/js/build.js
@@ -26,7 +26,6 @@
'js/factories/index',
'js/factories/manage_users',
'js/factories/outline',
- 'js/factories/settings',
'js/factories/settings_advanced'
]),
/**
diff --git a/cms/static/js/factories/settings.js b/cms/static/js/factories/settings.js
deleted file mode 100644
index 472f3c874e9e..000000000000
--- a/cms/static/js/factories/settings.js
+++ /dev/null
@@ -1,41 +0,0 @@
-define([
- 'jquery', 'js/models/settings/course_details', 'js/views/settings/main'
-], function($, CourseDetailsModel, MainView) {
- 'use strict';
-
- return function(detailsUrl, showMinGradeWarning, showCertificateAvailableDate, upgradeDeadline) {
- var model;
- // highlighting labels when fields are focused in
- $('form :input')
- .focus(function() {
- $('label[for="' + this.id + '"]').addClass('is-focused');
- })
- .blur(function() {
- $('label').removeClass('is-focused');
- });
-
- // Toggle collapsibles when trigger is clicked
- $('.collapsible .collapsible-trigger').click(function() {
- const contentId = this.id.replace('-trigger', '-content');
- $(`#${contentId}`).toggleClass('collapsed');
- });
-
- model = new CourseDetailsModel();
- model.urlRoot = detailsUrl;
- model.showCertificateAvailableDate = showCertificateAvailableDate;
- model.set('upgrade_deadline', upgradeDeadline);
- model.fetch({
- // eslint-disable-next-line no-shadow
- success: function(model) {
- var editor = new MainView({
- el: $('.settings-details'),
- model: model,
- showMinGradeWarning: showMinGradeWarning
- });
- editor.render();
- },
- reset: true,
- cache: false
- });
- };
-});
diff --git a/cms/templates/settings.html b/cms/templates/settings.html
deleted file mode 100644
index df64bcc39361..000000000000
--- a/cms/templates/settings.html
+++ /dev/null
@@ -1,723 +0,0 @@
-<%page expression_filter="h"/>
-<%inherit file="base.html" />
-<%def name="online_help_token()"><% return "schedule" %>%def>
-<%block name="title">${_("Schedule & Details Settings")}%block>
-<%block name="bodyclass">is-signedin course schedule view-settings feature-upload%block>
-
-<%namespace name='static' file='static_content.html'/>
-<%!
- from django.utils.translation import gettext as _
- from common.djangoapps.student.auth import has_studio_advanced_settings_access
- from cms.djangoapps.contentstore import utils
- from lms.djangoapps.certificates.api import can_show_certificate_available_date_field
- from openedx.core.djangolib.js_utils import (
- dump_js_escaped_json, js_escaped_string
- )
- from openedx.core.djangolib.markup import HTML, Text
- from six.moves.urllib.parse import quote
- from six.moves.urllib import parse as urllib
-%>
-
-<%block name="header_extras">
-% for template_name in ["basic-modal", "modal-button", "upload-dialog", "license-selector", "course-settings-learning-fields", "course-instructor-details"]:
-
-% endfor
-%block>
-
-<%block name="jsextra">
-
-
-
-%block>
-
-<%block name="requirejs">
- require(["js/factories/settings"], function(SettingsFactory) {
- SettingsFactory(
- "${details_url | n, js_escaped_string}",
- ${show_min_grade_warning | n, dump_js_escaped_json},
- ${can_show_certificate_available_date_field(context_course) | n, dump_js_escaped_json},
- "${upgrade_deadline | n, js_escaped_string}",
- );
- });
-%block>
-
-<%block name="content">
-
-
-
-
-
-
-
-
-
-
${_("How are these settings used?")}
-
${_("Your course's schedule determines when students can enroll in and begin a course.")}
-
-
${_("Other information from this page appears on the About page for your course. This information includes the course overview, course image, introduction video, and estimated time requirements. Students use About pages to choose new courses to take.")}
-
-
-
- % if context_course:
- <%
- url_encoded_course_id = quote(str(context_course.id).encode('utf-8'), safe='')
- course_team_url = utils.reverse_course_url('course_team_handler', context_course.id)
- grading_config_url = utils.reverse_course_url('grading_handler', context_course.id)
- advanced_config_url = utils.reverse_course_url('advanced_settings_handler', context_course.id)
- %>
-
${_("Other Course Settings")}
-
-
-
- % endif
-
-
-
-
-%block>
diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html
index 9ad8492919b4..0dcdce32ff5a 100644
--- a/cms/templates/widgets/header.html
+++ b/cms/templates/widgets/header.html
@@ -37,7 +37,6 @@
import_url = reverse('import_handler', kwargs={'course_key_string': str(course_key)})
course_info_url = reverse('course_info_handler', kwargs={'course_key_string': str(course_key)})
export_url = reverse('export_handler', kwargs={'course_key_string': str(course_key)})
- settings_url = reverse('settings_handler', kwargs={'course_key_string': str(course_key)})
advanced_settings_url = reverse('advanced_settings_handler', kwargs={'course_key_string': str(course_key)})
tabs_url = reverse('tabs_handler', kwargs={'course_key_string': str(course_key)})
certificates_url = ''
@@ -45,7 +44,6 @@
certificates_url = reverse('certificates_list_handler', kwargs={'course_key_string': str(course_key)})
checklists_url = reverse('checklists_handler', kwargs={'course_key_string': str(course_key)})
pages_and_resources_mfe_enabled = ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND.is_enabled(context_course.id)
- schedule_details_mfe_enabled = toggles.use_new_schedule_details_page(context_course.id)
course_team_mfe_enabled = toggles.use_new_course_team_page(context_course.id)
advanced_settings_mfe_enabled = toggles.use_new_advanced_settings_page(context_course.id)
import_mfe_enabled = toggles.use_new_import_page(context_course.id)
@@ -116,16 +114,9 @@ ${_("Course"
- % if not schedule_details_mfe_enabled:
-
- ${_("Schedule & Details")}
-
- % endif
- % if schedule_details_mfe_enabled:
${_("Schedule & Details")}
- % endif
${_("Grading")}
diff --git a/cms/urls.py b/cms/urls.py
index de35a09483ce..845f8f997248 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -166,6 +166,9 @@
contentstore_views.grading_handler, name='grading_handler'),
re_path(fr'^settings/advanced/{settings.COURSE_KEY_PATTERN}$', contentstore_views.advanced_settings_handler,
name='advanced_settings_handler'),
+ # Backward-compat redirect: old /settings/ URL now unconditionally goes to MFE.
+ # Must come after all other ^settings/... patterns so it doesn't shadow them.
+ re_path(fr'^settings/{settings.COURSE_KEY_PATTERN}$', contentstore_views.settings_handler),
re_path(fr'^textbooks/{settings.COURSE_KEY_PATTERN}$', contentstore_views.textbooks_list_handler,
name='textbooks_list_handler'),
re_path(fr'^textbooks/{settings.COURSE_KEY_PATTERN}/(?P\d[^/]*)$',
diff --git a/openedx/core/djangoapps/models/course_details.py b/openedx/core/djangoapps/models/course_details.py
index 22f3a5eb2ed0..757eb5d7c0f9 100644
--- a/openedx/core/djangoapps/models/course_details.py
+++ b/openedx/core/djangoapps/models/course_details.py
@@ -218,7 +218,7 @@ def update_from_json(cls, course_key, jsondict, user): # pylint: disable=too-ma
# is what the setter expects as input.
date = Date()
- if jsondict['overview'] == '':
+ if jsondict.get('overview') == '':
jsondict['overview'] = '
'
if 'start_date' in jsondict:
@@ -323,7 +323,8 @@ def update_from_json(cls, course_key, jsondict, user): # pylint: disable=too-ma
if attribute in jsondict:
cls.update_about_item(block, attribute, jsondict[attribute], user.id)
- cls.update_about_video(block, jsondict['intro_video'], user.id)
+ if 'intro_video' in jsondict:
+ cls.update_about_video(block, jsondict['intro_video'], user.id)
# Could just return jsondict w/o doing any db reads, but I put
# the reads in as a means to confirm it persisted correctly