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:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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};
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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},
|
||||
// ],
|
||||
|
||||
|
||||
@@ -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"> ›</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="title" href="${pages_and_resources_mfe_url}" rel="external">${_("Pages & Resources")}</a>
|
||||
<span class="spacer"> ›</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<h1 class="page-header">
|
||||
<span class="sr">> </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>
|
||||
@@ -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',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user