feat!: Drop the legacy Tabs UI (#37557)

This change drops the legacy studio custom pages UI aka. the tab edit
page.

This work is part of https://github.com/openedx/edx-platform/issues/36108

BREAKING CHANGE: The 'legacy_studio.custom_pages' waffle flag has been
removed and the code will work as if this flag is permanently set to
False.

Co-authored-by: Kyle McCormick <kyle@axim.org>
This commit is contained in:
Feanil Patel
2025-11-20 18:36:10 -05:00
committed by GitHub
parent d929cdb7fa
commit b1f01ed8fa
18 changed files with 28 additions and 912 deletions

View File

@@ -13,7 +13,6 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
from ..serializers import CourseTabSerializer, CourseTabUpdateSerializer, TabIDLocatorSerializer
from ....toggles import use_new_custom_pages
from ....views.tabs import edit_tab_handler, get_course_tabs, reorder_tabs_handler
@@ -79,24 +78,20 @@ class CourseTabListView(DeveloperErrorViewMixin, APIView):
```
"""
course_key = CourseKey.from_string(course_id)
if not use_new_custom_pages(course_key):
return Response(status=status.HTTP_403_FORBIDDEN)
if not has_studio_read_access(request.user, course_key):
self.permission_denied(request)
course_block = modulestore().get_course(course_key)
tabs_to_render = get_course_tabs(course_block, request.user)
serializedCourseTabs = CourseTabSerializer(tabs_to_render, many=True).data
if use_new_custom_pages(course_key):
json_tabs = []
for tab in serializedCourseTabs:
if tab.get('type') == 'static_tab':
url_slug = tab.get('settings').get('url_slug')
static_tab_loc = course_block.id.make_usage_key("static_tab", url_slug)
tab["id"] = str(static_tab_loc)
json_tabs.append(tab)
return Response(json_tabs)
return Response(serializedCourseTabs)
json_tabs = []
for tab in serializedCourseTabs:
if tab.get('type') == 'static_tab':
url_slug = tab.get('settings').get('url_slug')
static_tab_loc = course_block.id.make_usage_key("static_tab", url_slug)
tab["id"] = str(static_tab_loc)
json_tabs.append(tab)
return Response(json_tabs)
@view_auth_classes(is_authenticated=True)

View File

@@ -52,10 +52,15 @@ class CourseWaffleFlagsSerializer(serializers.Serializer):
def get_use_new_custom_pages(self, obj):
"""
Method to get the use_new_custom_pages switch
Method to indicate whether or not to use the new custom pages
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
"""
course_key = self.get_course_key()
return toggles.use_new_custom_pages(course_key)
return True
def get_use_new_schedule_details_page(self, obj):
"""

View File

@@ -1498,8 +1498,6 @@ class ContentStoreTest(ContentStoreTestCase):
test_get_html('export_handler')
with override_waffle_flag(toggles.LEGACY_STUDIO_COURSE_TEAM, True):
test_get_html('course_team_handler')
with override_waffle_flag(toggles.LEGACY_STUDIO_CUSTOM_PAGES, True):
test_get_html('tabs_handler')
with override_waffle_flag(toggles.LEGACY_STUDIO_SCHEDULE_DETAILS, True):
test_get_html('settings_handler')
with override_waffle_flag(toggles.LEGACY_STUDIO_GRADING, True):
@@ -1518,6 +1516,11 @@ class ContentStoreTest(ContentStoreTestCase):
resp = self.client.get(course_updates_url)
assert resp.status_code == 200
resp = self.client.get(
get_url('cms.djangoapps.contentstore:v0:course_tab_list', course_key, 'course_id')
)
self.assertEqual(resp.status_code, 200)
# go look at the Edit page
unit_key = course_key.make_usage_key('vertical', 'test_vertical')
with override_waffle_flag(toggles.LEGACY_STUDIO_UNIT_EDITOR, True):

View File

@@ -167,7 +167,6 @@ class CourseAdvanceSettingViewTest(CourseTestCase, MilestonesTestCaseMixin):
@override_waffle_flag(toggles.LEGACY_STUDIO_IMPORT, True)
@override_waffle_flag(toggles.LEGACY_STUDIO_EXPORT, True)
@override_waffle_flag(toggles.LEGACY_STUDIO_COURSE_TEAM, True)
@override_waffle_flag(toggles.LEGACY_STUDIO_CUSTOM_PAGES, True)
@override_waffle_flag(toggles.LEGACY_STUDIO_SCHEDULE_DETAILS, True)
@override_waffle_flag(toggles.LEGACY_STUDIO_GRADING, True)
def test_disable_advanced_settings_feature(self, disable_advanced_settings):
@@ -184,7 +183,6 @@ class CourseAdvanceSettingViewTest(CourseTestCase, MilestonesTestCaseMixin):
'import_handler',
'export_handler',
'course_team_handler',
'tabs_handler',
'settings_handler',
'grading_handler',
):

View File

@@ -162,25 +162,6 @@ def individualize_anonymous_user_id(course_id):
return INDIVIDUALIZE_ANONYMOUS_USER_ID.is_enabled(course_id)
# .. toggle_name: legacy_studio.custom_pages
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Temporarily fall back to the old Studio custom pages tab.
# .. 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_CUSTOM_PAGES = CourseWaffleFlag("legacy_studio.custom_pages", __name__)
def use_new_custom_pages(course_key):
"""
Returns a boolean if new studio custom pages mfe is enabled
"""
return not LEGACY_STUDIO_CUSTOM_PAGES.is_enabled(course_key)
# .. toggle_name: contentstore.use_react_markdown_editor
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False

View File

@@ -44,7 +44,6 @@ from cms.djangoapps.contentstore.toggles import (
use_new_advanced_settings_page,
use_new_certificates_page,
use_new_course_team_page,
use_new_custom_pages,
use_new_export_page,
use_new_grading_page,
use_new_group_configurations_page,
@@ -514,11 +513,10 @@ def get_custom_pages_url(course_locator) -> str:
Gets course authoring microfrontend URL for custom pages view.
"""
custom_pages_url = None
if use_new_custom_pages(course_locator):
mfe_base_url = get_course_authoring_url(course_locator)
course_mfe_url = f'{mfe_base_url}/course/{course_locator}/custom-pages'
if mfe_base_url:
custom_pages_url = course_mfe_url
mfe_base_url = get_course_authoring_url(course_locator)
course_mfe_url = f'{mfe_base_url}/course/{course_locator}/custom-pages'
if mfe_base_url:
custom_pages_url = course_mfe_url
return custom_pages_url

View File

@@ -17,11 +17,9 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException, StaticTab
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadRequest, expect_json
from ..toggles import use_new_custom_pages
from ..utils import get_lms_link_for_item, get_pages_and_resources_url, get_custom_pages_url
from ..utils import get_pages_and_resources_url, get_custom_pages_url
__all__ = ["tabs_handler", "update_tabs_handler"]
@@ -65,18 +63,7 @@ def tabs_handler(request, course_key_string):
elif request.method == "GET": # assume html
# get all tabs from the tabs list: static tabs (a.k.a. user-created tabs) and built-in tabs
# present in the same order they are displayed in LMS
if use_new_custom_pages(course_key):
return redirect(get_custom_pages_url(course_key))
tabs_to_render = list(get_course_tabs(course_item, request.user))
return render_to_response(
"edit-tabs.html",
{
"context_course": course_item,
"tabs_to_render": tabs_to_render,
"lms_link": get_lms_link_for_item(course_item.location),
},
)
return redirect(get_custom_pages_url(course_key))
else:
return HttpResponseNotFound()

View File

@@ -4,9 +4,6 @@
import json
import random
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 cms.djangoapps.contentstore.views import tabs
@@ -68,12 +65,11 @@ class TabsPageTests(CourseTestCase):
data={'invalid_request': None},
)
@override_waffle_flag(toggles.LEGACY_STUDIO_CUSTOM_PAGES, True)
def test_view_index(self):
"""Basic check that the Pages page responds correctly"""
resp = self.client.get_html(self.url)
self.assertContains(resp, 'course-nav-list')
assert resp.status_code == 302
def test_reorder_tabs(self):
"""Test re-ordering of tabs"""

View File

@@ -242,7 +242,6 @@
'js/spec/video/transcripts/editor_spec',
'js/spec/video/transcripts/file_uploader_spec',
'js/spec/models/component_template_spec',
'js/spec/models/explicit_url_spec',
'js/spec/models/xblock_info_spec',
'js/spec/models/xblock_validation_spec',
'js/spec/models/license_spec',

View File

@@ -1,27 +0,0 @@
import * as TabsModel from 'js/models/explicit_url';
import * as TabsEditView from 'js/views/tabs';
import * as xmoduleLoader from 'xmodule';
import './base';
import 'cms/js/main';
import 'xblock/cms.runtime.v1';
import 'xmodule/js/src/xmodule'; // Force the XBlockToXModuleShim to load for Static Tabs
// eslint-disable-next-line no-unused-expressions
'use strict';
export default function EditTabsFactory(courseLocation, explicitUrl) {
xmoduleLoader.done(function() {
var model = new TabsModel({
id: courseLocation,
explicit_url: explicitUrl
}),
editView;
editView = new TabsEditView({
el: $('.tab-list'),
model: model,
mast: $('.wrapper-mast')
});
});
}
export {EditTabsFactory};

View File

@@ -1,14 +0,0 @@
/**
* A model that simply allows the update URL to be passed
* in as an argument.
*/
define(['backbone'], function(Backbone) {
return Backbone.Model.extend({
defaults: {
explicit_url: ''
},
url: function() {
return this.get('explicit_url');
}
});
});

View File

@@ -1,12 +0,0 @@
define(['js/models/explicit_url'],
function(Model) {
describe('Model ', function() {
it('allows url to be passed in constructor', function() {
expect(new Model({explicit_url: '/fancy/url'}).url()).toBe('/fancy/url');
});
it('returns empty string if url not set', function() {
expect(new Model().url()).toBe('');
});
});
}
);

View File

@@ -1,266 +0,0 @@
import $ from 'jquery';
import ViewUtils from 'common/js/components/utils/view_utils';
import edit_helpers from 'js/spec_helpers/edit_helpers';
import ModuleEdit from 'js/views/module_edit';
import ModuleModel from 'js/models/module_info';
import 'xmodule/js/src/xmodule';
describe('ModuleEdit', function() {
beforeEach(function() {
this.stubModule = new ModuleModel({
id: 'stub-id'
});
setFixtures('<ul>\n'
+ '<li class="component" id="stub-id" data-locator="stub-id">\n'
+ ' <div class="component-editor">\n'
+ ' <div class="module-editor">\n'
// eslint-disable-next-line no-template-curly-in-string
+ ' ${editor}\n'
+ ' </div>\n'
+ ' <a href="#" class="save-button">Save</a>\n'
+ ' <a href="#" class="cancel-button">Cancel</a>\n'
+ ' </div>\n'
+ ' <div class="component-actions">\n'
+ ' <a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>\n'
+ ' <a href="#" class="delete-button"><span class="delete-icon white">'
+ '</span>Delete</a>\n'
+ ' </div>\n'
+ ' <span class="drag-handle action"></span>\n'
+ ' <section class="xblock xblock-student_view xmodule_display xmodule_stub"'
+ ' data-type="StubModule">\n'
+ ' <div id="stub-module-content"/>\n'
+ ' </section>\n'
+ '</li>\n'
+ '</ul>');
edit_helpers.installEditTemplates(true);
spyOn($, 'ajax').and.returnValue(this.moduleData);
this.moduleEdit = new ModuleEdit({
el: $('.component'),
model: this.stubModule,
onDelete: jasmine.createSpy()
});
return this.moduleEdit;
});
describe('class definition', function() {
it('sets the correct tagName', function() {
return expect(this.moduleEdit.tagName).toEqual('li');
});
it('sets the correct className', function() {
return expect(this.moduleEdit.className).toEqual('component');
});
});
describe('methods', function() {
describe('initialize', function() {
beforeEach(function() {
spyOn(ModuleEdit.prototype, 'render');
this.moduleEdit = new ModuleEdit({
el: $('.component'),
model: this.stubModule,
onDelete: jasmine.createSpy()
});
return this.moduleEdit;
});
it('renders the module editor', function() {
return expect(ModuleEdit.prototype.render).toHaveBeenCalled();
});
});
describe('render', function() {
beforeEach(function() {
edit_helpers.installEditTemplates(true);
spyOn(this.moduleEdit, 'loadDisplay');
spyOn(this.moduleEdit, 'delegateEvents');
spyOn($.fn, 'append');
spyOn(ViewUtils, 'loadJavaScript').and.returnValue($.Deferred().resolve().promise());
window.MockXBlock = function() {
return {};
};
// eslint-disable-next-line no-void
window.loadedXBlockResources = void 0;
this.moduleEdit.render();
return $.ajax.calls.mostRecent().args[0].success({
html: '<div>Response html</div>',
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
], [
'hash2', {
kind: 'url',
mimetype: 'text/css',
data: 'css-url'
}
], [
'hash3', {
kind: 'text',
mimetype: 'application/javascript',
data: 'inline-js'
}
], [
'hash4', {
kind: 'url',
mimetype: 'application/javascript',
data: 'js-url'
}
], [
'hash5', {
placement: 'head',
mimetype: 'text/html',
data: 'head-html'
}
], [
'hash6', {
placement: 'not-head',
mimetype: 'text/html',
data: 'not-head-html'
}
]
]
});
});
afterEach(function() {
window.MockXBlock = null;
return window.MockXBlock;
});
it('loads the module preview via ajax on the view element', function() {
expect($.ajax).toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/student_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
expect($.ajax).not.toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
expect(this.moduleEdit.loadDisplay).toHaveBeenCalled();
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
});
it('loads the editing view via ajax on demand', function() {
var mockXBlockEditorHtml;
expect($.ajax).not.toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
this.moduleEdit.clickEditButton({
preventDefault: jasmine.createSpy('event.preventDefault')
});
mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore');
$.ajax.calls.mostRecent().args[0].success({
html: mockXBlockEditorHtml,
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
], [
'hash2', {
kind: 'url',
mimetype: 'text/css',
data: 'css-url'
}
], [
'hash3', {
kind: 'text',
mimetype: 'application/javascript',
data: 'inline-js'
}
], [
'hash4', {
kind: 'url',
mimetype: 'application/javascript',
data: 'js-url'
}
], [
'hash5', {
placement: 'head',
mimetype: 'text/html',
data: 'head-html'
}
], [
'hash6', {
placement: 'not-head',
mimetype: 'text/html',
data: 'not-head-html'
}
]
]
});
expect($.ajax).toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
});
it('loads inline css from fragments', function() {
var args = '<style type="text/css">inline-css</style>';
return expect($('head').append).toHaveBeenCalledWith(args);
});
it('loads css urls from fragments', function() {
var args = '<link rel="stylesheet" href="css-url" type="text/css">';
return expect($('head').append).toHaveBeenCalledWith(args);
});
it('loads inline js from fragments', function() {
return expect($('head').append).toHaveBeenCalledWith('<script>inline-js</script>');
});
it('loads js urls from fragments', function() {
return expect(ViewUtils.loadJavaScript).toHaveBeenCalledWith('js-url');
});
it('loads head html', function() {
return expect($('head').append).toHaveBeenCalledWith('head-html');
});
it("doesn't load body html", function() {
return expect($.fn.append).not.toHaveBeenCalledWith('not-head-html');
});
it("doesn't reload resources", function() {
var count;
count = $('head').append.calls.count();
$.ajax.calls.mostRecent().args[0].success({
html: '<div>Response html 2</div>',
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
]
]
});
return expect($('head').append.calls.count()).toBe(count);
});
});
describe('loadDisplay', function() {
beforeEach(function() {
spyOn(XBlock, 'initializeBlock');
return this.moduleEdit.loadDisplay();
});
it('loads the .xmodule-display inside the module editor', function() {
expect(XBlock.initializeBlock).toHaveBeenCalled();
var sel = '.xblock-student_view';
return expect(XBlock.initializeBlock.calls.mostRecent().args[0].get(0)).toBe($(sel).get(0));
});
});
});
});

View File

@@ -1,109 +0,0 @@
(function() {
'use strict';
var __hasProp = {}.hasOwnProperty,
__extends = function(child, parent) {
var key;
for (key in parent) {
if (__hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
function Ctor() {
this.constructor = child;
}
Ctor.prototype = parent.prototype;
child.prototype = new Ctor();
child.__super__ = parent.prototype;
return child;
};
define(['jquery', 'underscore', 'gettext', 'xblock/runtime.v1', 'js/views/xblock', 'js/views/modals/edit_xblock'],
function($, _, gettext, XBlock, XBlockView, EditXBlockModal) {
var ModuleEdit = (function(_super) {
__extends(ModuleEdit, _super);
// eslint-disable-next-line no-shadow
function ModuleEdit() {
return ModuleEdit.__super__.constructor.apply(this, arguments);
}
ModuleEdit.prototype.tagName = 'li';
ModuleEdit.prototype.className = 'component';
ModuleEdit.prototype.editorMode = 'editor-mode';
ModuleEdit.prototype.events = {
'click .edit-button': 'clickEditButton',
'click .delete-button': 'onDelete'
};
ModuleEdit.prototype.initialize = function() {
this.onDelete = this.options.onDelete;
return this.render();
};
ModuleEdit.prototype.loadDisplay = function() {
var xblockElement;
xblockElement = this.$el.find('.xblock-student_view');
if (xblockElement.length > 0) {
return XBlock.initializeBlock(xblockElement);
}
};
ModuleEdit.prototype.createItem = function(parent, payload, callback) {
var _this = this;
if (_.isNull(callback)) {
callback = function() {};
}
payload.parent_locator = parent;
return $.postJSON(this.model.urlRoot + '/', payload, function(data) {
_this.model.set({
id: data.locator
});
_this.$el.data('locator', data.locator);
_this.$el.data('courseKey', data.courseKey);
return _this.render();
}).success(callback);
};
ModuleEdit.prototype.loadView = function(viewName, target, callback) {
var _this = this;
if (this.model.id) {
return $.ajax({
url: '' + (decodeURIComponent(this.model.url())) + '/' + viewName,
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: function(fragment) {
return _this.renderXBlockFragment(fragment, target).done(callback);
}
});
}
};
ModuleEdit.prototype.render = function() {
var _this = this;
return this.loadView('student_view', this.$el, function() {
_this.loadDisplay();
return _this.delegateEvents();
});
};
ModuleEdit.prototype.clickEditButton = function(event) {
var modal;
event.preventDefault();
modal = new EditXBlockModal();
return modal.edit(this.$el, this.model, {
refresh: _.bind(this.render, this)
});
};
return ModuleEdit;
}(XBlockView));
return ModuleEdit;
});
}).call(this);

View File

@@ -1,203 +0,0 @@
/* globals analytics, course_location_analytics */
(function(analytics, course_location_analytics) {
'use strict';
var __hasProp = {}.hasOwnProperty,
__extends = function(child, parent) {
var key;
for (key in parent) {
if (__hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
function Ctor() {
this.constructor = child;
}
Ctor.prototype = parent.prototype;
child.prototype = new Ctor();
child.__super__ = parent.prototype;
return child;
};
define(['underscore', 'jquery', 'jquery.ui', 'backbone', 'common/js/components/views/feedback_prompt',
'common/js/components/views/feedback_notification', 'js/views/module_edit',
'js/models/module_info', 'js/utils/module'],
function(_, $, ui, Backbone, PromptView, NotificationView, ModuleEditView, ModuleModel, ModuleUtils) {
var TabsEdit;
TabsEdit = (function(_super) {
__extends(TabsEdit, _super);
// eslint-disable-next-line no-shadow
function TabsEdit() {
var self = this;
this.deleteTab = function() {
return TabsEdit.prototype.deleteTab.apply(self, arguments);
};
this.addNewTab = function() {
return TabsEdit.prototype.addNewTab.apply(self, arguments);
};
this.tabMoved = function() {
return TabsEdit.prototype.tabMoved.apply(self, arguments);
};
this.toggleVisibilityOfTab = function() {
return TabsEdit.prototype.toggleVisibilityOfTab.apply(self, arguments);
};
this.initialize = function() {
return TabsEdit.prototype.initialize.apply(self, arguments);
};
return TabsEdit.__super__.constructor.apply(this, arguments);
}
TabsEdit.prototype.initialize = function(options) {
var self = this;
this.$('.component').each(function(idx, element) {
var model;
model = new ModuleModel({
id: $(element).data('locator')
});
return new ModuleEditView({
el: element,
onDelete: self.deleteTab,
model: model
});
});
this.options = _.extend({}, options);
this.options.mast.find('.new-tab').on('click', this.addNewTab);
$('.add-pages .new-tab').on('click', this.addNewTab);
$('.toggle-checkbox').on('click', this.toggleVisibilityOfTab);
return this.$('.course-nav-list').sortable({
handle: '.drag-handle',
update: this.tabMoved,
helper: 'clone',
opacity: '0.5',
placeholder: 'component-placeholder',
forcePlaceholderSize: true,
axis: 'y',
items: '> .is-movable'
});
};
TabsEdit.prototype.toggleVisibilityOfTab = function(event) {
var checkbox_element, saving, tab_element;
checkbox_element = event.target;
tab_element = $(checkbox_element).parents('.course-tab')[0];
saving = new NotificationView.Mini({
title: gettext('Saving')
});
saving.show();
return $.ajax({
type: 'POST',
url: this.model.url(),
data: JSON.stringify({
tab_id_locator: {
tab_id: $(tab_element).data('tab-id'),
tab_locator: $(tab_element).data('locator')
},
is_hidden: $(checkbox_element).is(':checked')
}),
contentType: 'application/json'
}).success(function() {
return saving.hide();
});
};
TabsEdit.prototype.tabMoved = function() {
var saving, tabs;
tabs = [];
this.$('.course-tab').each(function(idx, element) {
return tabs.push({
tab_id: $(element).data('tab-id'),
tab_locator: $(element).data('locator')
});
});
analytics.track('Reordered Pages', {
course: course_location_analytics
});
saving = new NotificationView.Mini({
title: gettext('Saving')
});
saving.show();
return $.ajax({
type: 'POST',
url: this.model.url(),
data: JSON.stringify({
tabs: tabs
}),
contentType: 'application/json'
}).success(function() {
return saving.hide();
});
};
TabsEdit.prototype.addNewTab = function(event) {
var editor;
event.preventDefault();
editor = new ModuleEditView({
onDelete: this.deleteTab,
model: new ModuleModel()
});
$('.new-component-item').before(editor.$el);
editor.$el.addClass('course-tab is-movable');
editor.$el.addClass('new');
setTimeout(function() {
return editor.$el.removeClass('new');
}, 1000);
$('html, body').animate({
scrollTop: $('.new-component-item').offset().top
}, 500);
editor.createItem(this.model.get('id'), {
category: 'static_tab'
});
return analytics.track('Added Page', {
course: course_location_analytics
});
};
TabsEdit.prototype.deleteTab = function(event) {
var confirm;
confirm = new PromptView.Warning({
title: gettext('Delete Page Confirmation'),
message: gettext('Are you sure you want to delete this page? This action cannot be undone.'),
actions: {
primary: {
text: gettext('OK'),
click: function(view) {
var $component, deleting;
view.hide();
$component = $(event.currentTarget).parents('.component');
analytics.track('Deleted Page', {
course: course_location_analytics,
id: $component.data('locator')
});
deleting = new NotificationView.Mini({
title: gettext('Deleting')
});
deleting.show();
return $.ajax({
type: 'DELETE',
url: ModuleUtils.getUpdateUrl($component.data('locator'))
}).success(function() {
$component.remove();
return deleting.hide();
});
}
},
secondary: [
{
text: gettext('Cancel'),
click: function(view) {
return view.hide();
}
}
]
}
});
return confirm.show();
};
return TabsEdit;
}(Backbone.View));
return TabsEdit;
});
}).call(this, analytics, course_location_analytics);

View File

@@ -25,7 +25,6 @@ var options = {
// {pattern: 'js/factories/xblock_validation.js', webpack: true},
// {pattern: 'js/factories/container.js', webpack: true},
// {pattern: 'js/factories/context_course.js', webpack: true},
// {pattern: 'js/factories/edit_tabs.js', webpack: true},
// {pattern: 'js/factories/library.js', webpack: true},
// ],

View File

@@ -1,213 +0,0 @@
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "pages" %></%def>
<%namespace name='static' file='static_content.html'/>
<%!
from django.utils.translation import gettext as _
from django.urls import reverse
from xmodule.tabs import StaticTab
from openedx.core.djangolib.js_utils import js_escaped_string
from cms.djangoapps.contentstore.utils import get_pages_and_resources_url
%>
<%block name="title">${_("Custom pages")}</%block>
<%block name="bodyclass">is-signedin course view-static-pages</%block>
<%block name="header_extras">
% for template_name in ["basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
</%block>
<%block name="page_bundle">
<%static:webpack entry="js/factories/edit_tabs">
EditTabsFactory("${context_course.location | n, js_escaped_string}", "${reverse('tabs_handler', kwargs={'course_key_string': context_course.id}) | n, js_escaped_string}");
</%static:webpack>
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
% if context_course:
<%
pages_and_resources_mfe_url = get_pages_and_resources_url(context_course.id)
pages_and_resources_mfe_enabled = bool(pages_and_resources_mfe_url)
%>
% endif
% if pages_and_resources_mfe_enabled:
<header class="mast has-actions">
<div class="jump-nav">
<nav class="nav-dd title ui-left">
<ol>
<li class="nav-item">
<span class="label">${_("Content")}</span>
<span class="spacer"> &rsaquo;</span>
</li>
<li class="nav-item">
<a class="title" href="${pages_and_resources_mfe_url}" rel="external">${_("Pages & Resources")}</a>
<span class="spacer"> &rsaquo;</span>
</li>
</ol>
</nav>
</div>
<h1 class="page-header">
<span class="sr">&gt; </span>${_("Custom pages")}
</h1>
% else:
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">${_("Content")}</small>
## Translators: Custom Pages refer to the tabs that appear in the top navigation of each course.
<span class="sr"> > </span>${_("Pages")}
</h1>
% endif
<nav class="nav-actions" aria-label="${_('Page Actions')}">
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
<li class="nav-item">
<a href="#" class="button new-button new-tab"><span class="icon fa fa-plus" aria-hidden="true"></span> ${_("New Page")}</a>
</li>
<li class="nav-item">
<a href="${lms_link}" rel="external" class="button view-button view-live-button">${_("View Live")}</a>
</li>
</ul>
</nav>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<div class="notice-incontext">
<p class="copy">${_("Note: Pages are publicly visible. If users know the URL of a page, they can view the page even if they are not registered for or logged in to your course.")}</p>
</div>
<div class="inner-wrapper">
<article class="unit-body">
<div class="tab-list">
<ol class="course-nav-list course components">
% for tab in tabs_to_render:
<%
css_class = "course-tab"
if tab.is_movable:
css_class = css_class + " is-movable"
elif (not tab.is_movable) and (not tab.is_hideable):
css_class = css_class + " is-fixed"
%>
% if isinstance(tab, StaticTab):
<li class="component ${css_class}" data-locator="${tab.locator}" data-tab-id="${tab.tab_id}"></li>
% else:
<li class="course-nav-item ${css_class}" data-tab-id="${tab.tab_id}">
<div class="course-nav-item-header">
% if tab.is_collection:
<h3 class="title-sub">${_(tab.name)}</h3>
<ul class="course-nav-item-children">
% for item in tab.items(context_course):
<li class="course-nav-item-child title">
${_(item.name)}
</li>
% endfor
</ul>
% else:
<h3 class="title">${_(tab.name)}</h3>
% endif
</div>
<div class="course-nav-item-actions wrapper-actions-list">
<ul class="actions-list">
% if tab.is_hideable:
<li class="action-item action-visible">
<label><span class="sr">${_("Show this page")}</span></label>
% if tab.is_hidden:
<input type="checkbox" class="toggle-checkbox" data-tooltip="${_('Show/hide page')}" checked />
% else:
<input type="checkbox" class="toggle-checkbox" data-tooltip="${_('Show/hide page')}" />
% endif
<div class="action-button"><span class="icon fa fa-eye" aria-hidden="true"></span><span class="icon fa fa-eye-slash"></span></div>
</li>
% endif
</ul>
</div>
% if tab.is_movable:
<div class="drag-handle action" data-tooltip="${_('Drag to reorder')}">
<span class="sr">${_("Drag to reorder")}</span>
</div>
% else:
<div class="drag-handle is-fixed" data-tooltip="${_('This page cannot be reordered')}">
<span class="sr">${_("This page cannot be reordered")}</span>
</div>
% endif
</li>
% endif
% endfor
<li class="new-component-item"></li>
</ol>
</div>
<div class="add-pages">
<p>${_("You can add additional custom pages to your course.")} <a href="#" class="button new-button new-tab"><span class="icon fa fa-plus" aria-hidden="true"></span>${_("Add a New Page")}</a></p>
</div>
</article>
</div>
</article>
% if pages_and_resources_mfe_enabled:
<aside class="content-supplementary" role="complementary">
<div class="bit">
<h3 class="title-3">${_("What are pages?")}</h3>
<p>${_("Pages are listed horizontally at the top of your course. Default pages (Home, Course, Discussion, Wiki, and Progress) are followed by textbooks and custom pages that you create.")}</p>
</div>
<div class="bit">
<h3 class="title-3">${_("Custom pages")}</h3>
<p>${_("You can create and edit custom pages to provide students with additional course content. For example, you can create pages for the grading policy, course slides, and a course calendar. ")} </p>
</div>
<div class="bit">
<h3 class="title-3">${_("How do pages look to students in my course?")}</h3>
<p>${_("Students see the default and custom pages at the top of your course and use these links to navigate.")} <br /> <a rel="modal" href="#preview-lms-staticpages">${_("See an example")}</a></p>
</div>
</aside>
% else:
<aside class="content-supplementary" role="complementary">
<div class="bit">
<h3 class="title-3">${_("What are custom pages?")}</h3>
<p>${_("You can create and edit custom pages to provide students with additional course content. For example, you can create pages for the grading policy, course syllabus, and a course calendar. ")} </p>
</div>
<div class="bit">
<h3 class="title-3">${_("How do custom pages look to students in my course?")}</h3>
<p>${_("Custom pages are listed horizontally at the top of your course after default pages.")} <br /> <a rel="modal" href="#preview-lms-staticpages">${_("See an example")}</a></p>
</div>
</aside>
% endif
</section>
</div>
<div class="content-modal" id="preview-lms-staticpages">
<h3 class="title">${_("Pages in Your Course")}</h3>
<figure>
<img src="${static.url("images/preview-lms-staticpages.png")}" alt="${_('Preview of Pages in your course')}" />
<figcaption class="description">${_("Pages appear in your course's top navigation bar. The default pages (Home, Course, Discussion, Wiki, and Progress) are followed by textbooks and custom pages.")}</figcaption>
</figure>
<a href="#" rel="view" class="action action-modal-close">
<span class="icon fa fa-times-circle" aria-hidden="true"></span>
<span class="label">${_("close modal")}</span>
</a>
</div>
</%block>

View File

@@ -104,7 +104,6 @@ module.exports = Merge.merge({
'js/factories/context_course': './cms/static/js/factories/context_course.js',
'js/factories/library': './cms/static/js/factories/library.js',
'js/factories/xblock_validation': './cms/static/js/factories/xblock_validation.js',
'js/factories/edit_tabs': './cms/static/js/factories/edit_tabs.js',
'js/sock': './cms/static/js/sock.js',
'js/factories/tag_count': './cms/static/js/factories/tag_count.js',