feat!: Drop the legacy studio updates page. (#37544)
Remove the legacy studio updates page and its related artifacts. They have been replaced by API and a new UI in the authoring MFE. This cleanup is a part of https://github.com/openedx/edx-platform/issues/36108 BREAKING CHANGE: The 'legacy_studio.updates' waffle flag will no longer be respected. The system will behave as if the flag is set to false permanently.
This commit is contained in:
@@ -80,10 +80,15 @@ class CourseWaffleFlagsSerializer(serializers.Serializer):
|
||||
|
||||
def get_use_new_updates_page(self, obj):
|
||||
"""
|
||||
Method to get the use_new_updates_page switch
|
||||
Method to indicate if we should use the new updates_page
|
||||
|
||||
This used to be based on a waffle flag but the flag is being removed so we
|
||||
default it to true for now until we can remove the need for it from the consumers
|
||||
of this serializer and the related APIs.
|
||||
|
||||
See https://github.com/openedx/edx-platform/issues/37497
|
||||
"""
|
||||
course_key = self.get_course_key()
|
||||
return toggles.use_new_updates_page(course_key)
|
||||
return True
|
||||
|
||||
def get_use_new_import_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_UPDATES, True):
|
||||
test_get_html('course_info_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):
|
||||
@@ -1510,6 +1508,16 @@ class ContentStoreTest(ContentStoreTestCase):
|
||||
test_get_html('advanced_settings_handler')
|
||||
test_get_json('textbooks_list_handler')
|
||||
|
||||
# Test that studio updates load
|
||||
course_updates_url = reverse(
|
||||
'course_info_update_handler',
|
||||
kwargs={
|
||||
'course_key_string': str(course_key),
|
||||
}
|
||||
)
|
||||
resp = self.client.get(course_updates_url)
|
||||
assert 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_UPDATES, 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)
|
||||
@@ -185,7 +184,6 @@ class CourseAdvanceSettingViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
'import_handler',
|
||||
'export_handler',
|
||||
'course_team_handler',
|
||||
'course_info_handler',
|
||||
'tabs_handler',
|
||||
'settings_handler',
|
||||
'grading_handler',
|
||||
|
||||
@@ -256,25 +256,6 @@ def use_new_grading_page(course_key):
|
||||
return not LEGACY_STUDIO_GRADING.is_enabled(course_key)
|
||||
|
||||
|
||||
# .. toggle_name: legacy_studio.updates
|
||||
# .. toggle_implementation: WaffleFlag
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Temporarily fall back to the old Studio Course Updates 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_UPDATES = CourseWaffleFlag('legacy_studio.updates', __name__)
|
||||
|
||||
|
||||
def use_new_updates_page(course_key):
|
||||
"""
|
||||
Returns a boolean if new studio updates mfe is enabled
|
||||
"""
|
||||
return not LEGACY_STUDIO_UPDATES.is_enabled(course_key)
|
||||
|
||||
|
||||
# .. toggle_name: legacy_studio.import
|
||||
# .. toggle_implementation: WaffleFlag
|
||||
# .. toggle_default: False
|
||||
|
||||
@@ -51,7 +51,6 @@ from cms.djangoapps.contentstore.toggles import (
|
||||
use_new_import_page,
|
||||
use_new_schedule_details_page,
|
||||
use_new_unit_page,
|
||||
use_new_updates_page,
|
||||
use_new_video_uploads_page,
|
||||
)
|
||||
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
|
||||
@@ -362,11 +361,10 @@ def get_updates_url(course_locator) -> str:
|
||||
Gets course authoring microfrontend URL for updates page view.
|
||||
"""
|
||||
updates_url = None
|
||||
if use_new_updates_page(course_locator):
|
||||
mfe_base_url = get_course_authoring_url(course_locator)
|
||||
course_mfe_url = f'{mfe_base_url}/course/{course_locator}/course_info'
|
||||
if mfe_base_url:
|
||||
updates_url = course_mfe_url
|
||||
mfe_base_url = get_course_authoring_url(course_locator)
|
||||
course_mfe_url = f'{mfe_base_url}/course/{course_locator}/course_info'
|
||||
if mfe_base_url:
|
||||
updates_url = course_mfe_url
|
||||
return updates_url
|
||||
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json
|
||||
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
||||
from organizations.models import Organization
|
||||
from xmodule.contentstore.content import StaticContent # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.course_block import CourseBlock, CourseFields # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.error_block import ErrorBlock # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore import EdxJSONEncoder # lint-amnesty, pylint: disable=wrong-import-order
|
||||
@@ -90,7 +89,6 @@ from ..courseware_index import CoursewareSearchIndexer, SearchIndexingError
|
||||
from ..tasks import rerun_course as rerun_course_task
|
||||
from ..toggles import (
|
||||
default_enable_flexible_peer_openassessments,
|
||||
use_new_updates_page,
|
||||
use_new_advanced_settings_page,
|
||||
use_new_grading_page,
|
||||
use_new_group_configurations_page,
|
||||
@@ -1064,24 +1062,7 @@ def course_info_handler(request, course_key_string):
|
||||
except InvalidKeyError:
|
||||
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
with modulestore().bulk_operations(course_key):
|
||||
course_block = get_course_and_check_access(course_key, request.user)
|
||||
if not course_block:
|
||||
raise Http404
|
||||
if use_new_updates_page(course_key):
|
||||
return redirect(get_updates_url(course_key))
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
return render_to_response(
|
||||
'course_info.html',
|
||||
{
|
||||
'context_course': course_block,
|
||||
'updates_url': reverse_course_url('course_info_update_handler', course_key),
|
||||
'handouts_locator': course_key.make_usage_key('course_info', 'handouts'),
|
||||
'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_block.id),
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest("Only supports html requests")
|
||||
return redirect(get_updates_url(course_key))
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
@@ -5,9 +5,7 @@ unit tests for course_info views and models.
|
||||
|
||||
import json
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
|
||||
from cms.djangoapps.contentstore import toggles
|
||||
from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase
|
||||
from cms.djangoapps.contentstore.utils import reverse_course_url, reverse_usage_url
|
||||
from openedx.core.lib.xblock_utils import get_course_update_items
|
||||
@@ -23,7 +21,6 @@ class CourseUpdateTest(CourseTestCase): # lint-amnesty, pylint: disable=missing
|
||||
return reverse_course_url('course_info_update_handler', course_key, kwargs=kwargs)
|
||||
|
||||
# The do all and end all of unit test cases.
|
||||
@override_waffle_flag(toggles.LEGACY_STUDIO_UPDATES, True)
|
||||
def test_course_update(self):
|
||||
"""Go through each interface and ensure it works."""
|
||||
def get_response(content, date):
|
||||
@@ -40,11 +37,6 @@ class CourseUpdateTest(CourseTestCase): # lint-amnesty, pylint: disable=missing
|
||||
|
||||
return json.loads(resp.content.decode('utf-8'))
|
||||
|
||||
resp = self.client.get_html(
|
||||
reverse_course_url('course_info_handler', self.course.id)
|
||||
)
|
||||
self.assertContains(resp, 'Course Updates', status_code=200)
|
||||
|
||||
init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">' # lint-amnesty, pylint: disable=line-too-long
|
||||
content = init_content + '</iframe>'
|
||||
payload = get_response(content, 'January 8, 2013')
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
'js/factories/asset_index',
|
||||
'js/factories/base',
|
||||
'js/factories/course_create_rerun',
|
||||
'js/factories/course_info',
|
||||
'js/factories/export',
|
||||
'js/factories/group_configurations',
|
||||
'js/certificates/factories/certificates_page_factory',
|
||||
|
||||
@@ -235,7 +235,6 @@
|
||||
'js/spec/models/settings_course_grader_spec',
|
||||
'js/spec/models/settings_grading_spec',
|
||||
'js/spec/models/upload_spec',
|
||||
'js/spec/views/course_info_spec',
|
||||
'js/spec/views/metadata_edit_spec',
|
||||
'js/spec/views/upload_spec',
|
||||
'js/spec/video/transcripts/message_manager_spec',
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
define(['backbone', 'js/models/course_update'], function(Backbone, CourseUpdateModel) {
|
||||
/*
|
||||
The intitializer of this collection must set id to the update's location.url and courseLocation to the course's location. Must pass the
|
||||
collection of updates as [{ date : "month day", content : "html"}]
|
||||
*/
|
||||
var CourseUpdateCollection = Backbone.Collection.extend({
|
||||
// instantiator must set url
|
||||
|
||||
model: CourseUpdateModel
|
||||
});
|
||||
return CourseUpdateCollection;
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
define([
|
||||
'jquery', 'js/collections/course_update', 'js/models/module_info',
|
||||
'js/models/course_info', 'js/views/course_info_edit'
|
||||
], function($, CourseUpdateCollection, ModuleInfoModel, CourseInfoModel, CourseInfoEditView) {
|
||||
'use strict';
|
||||
|
||||
return function(updatesUrl, handoutsLocator, baseAssetUrl) {
|
||||
var course_updates = new CourseUpdateCollection(),
|
||||
course_handouts, editor;
|
||||
|
||||
course_updates.url = updatesUrl;
|
||||
course_updates.fetch({reset: true});
|
||||
course_handouts = new ModuleInfoModel({
|
||||
id: handoutsLocator
|
||||
});
|
||||
editor = new CourseInfoEditView({
|
||||
el: $('.main-wrapper'),
|
||||
model: new CourseInfoModel({
|
||||
updates: course_updates,
|
||||
base_asset_url: baseAssetUrl,
|
||||
handouts: course_handouts
|
||||
})
|
||||
});
|
||||
editor.render();
|
||||
};
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
define(['backbone'], function(Backbone) {
|
||||
// single per course holds the updates and handouts
|
||||
var CourseInfo = Backbone.Model.extend({
|
||||
// This model class is not suited for restful operations and is considered just a server side initialized container
|
||||
url: '',
|
||||
|
||||
defaults: {
|
||||
updates: null, // UpdateCollection
|
||||
handouts: null // HandoutCollection
|
||||
}
|
||||
});
|
||||
return CourseInfo;
|
||||
});
|
||||
@@ -1,283 +0,0 @@
|
||||
define(["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info",
|
||||
"js/collections/course_update", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers"],
|
||||
(CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, AjaxHelpers) =>
|
||||
|
||||
describe("Course Updates and Handouts", function() {
|
||||
const courseInfoPage = `\
|
||||
<div class="course-info-wrapper">
|
||||
<div class="main-column window">
|
||||
<article class="course-updates" id="course-update-view">
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
</article>
|
||||
</div>
|
||||
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
|
||||
</div>
|
||||
<div class="modal-cover"></div>\
|
||||
`;
|
||||
|
||||
beforeEach(function() {
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track']);
|
||||
window.course_location_analytics = jasmine.createSpy();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
delete window.analytics;
|
||||
delete window.course_location_analytics;
|
||||
});
|
||||
|
||||
describe("Course Updates", function() {
|
||||
const courseInfoTemplate = readFixtures('course_info_update.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
let cancelEditingUpdate;
|
||||
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate));
|
||||
appendSetFixtures(courseInfoPage);
|
||||
|
||||
this.collection = new CourseUpdateCollection();
|
||||
this.collection.url = 'course_info_update/';
|
||||
this.courseInfoEdit = new CourseInfoUpdateView({
|
||||
el: $('.course-updates'),
|
||||
collection: this.collection,
|
||||
base_asset_url : 'base-asset-url/'
|
||||
});
|
||||
|
||||
this.courseInfoEdit.render();
|
||||
|
||||
this.event = {
|
||||
preventDefault() { return 'no op'; }
|
||||
};
|
||||
|
||||
this.createNewUpdate = function(text) {
|
||||
// Edit button is not in the template under test (it is in parent HTML).
|
||||
// Therefore call onNew directly.
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
spyOn(this.courseInfoEdit.$codeMirror, 'getValue').and.returnValue(text);
|
||||
return this.courseInfoEdit.$el.find('.save-button').click();
|
||||
};
|
||||
|
||||
this.cancelNewCourseInfo = function(useCancelButton) {
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
spyOn(this.courseInfoEdit.$modalCover, 'hide').and.callThrough();
|
||||
|
||||
spyOn(this.courseInfoEdit.$codeMirror, 'getValue').and.returnValue('unsaved changes');
|
||||
const model = this.collection.at(0);
|
||||
spyOn(model, "save").and.callThrough();
|
||||
|
||||
cancelEditingUpdate(this.courseInfoEdit, this.courseInfoEdit.$modalCover, useCancelButton);
|
||||
|
||||
expect(this.courseInfoEdit.$modalCover.hide).toHaveBeenCalled();
|
||||
expect(model.save).not.toHaveBeenCalled();
|
||||
const previewContents = this.courseInfoEdit.$el.find('.update-contents').html();
|
||||
expect(previewContents).not.toEqual('unsaved changes');
|
||||
};
|
||||
|
||||
this.doNotCloseNewCourseInfo = function() {
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
spyOn(this.courseInfoEdit.$modalCover, 'hide').and.callThrough();
|
||||
|
||||
spyOn(this.courseInfoEdit.$codeMirror, 'getValue').and.returnValue('unsaved changes');
|
||||
const model = this.collection.at(0);
|
||||
spyOn(model, "save").and.callThrough();
|
||||
|
||||
cancelEditingUpdate(this.courseInfoEdit, this.courseInfoEdit.$modalCover, false);
|
||||
|
||||
expect(model.save).not.toHaveBeenCalled();
|
||||
expect(this.courseInfoEdit.$modalCover.hide).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
this.cancelExistingCourseInfo = function(useCancelButton) {
|
||||
this.createNewUpdate('existing update');
|
||||
this.courseInfoEdit.$el.find('.edit-button').click();
|
||||
spyOn(this.courseInfoEdit.$modalCover, 'hide').and.callThrough();
|
||||
|
||||
spyOn(this.courseInfoEdit.$codeMirror, 'getValue').and.returnValue('modification');
|
||||
const model = this.collection.at(0);
|
||||
spyOn(model, "save").and.callThrough();
|
||||
model.id = "saved_to_server";
|
||||
cancelEditingUpdate(this.courseInfoEdit, this.courseInfoEdit.$modalCover, useCancelButton);
|
||||
|
||||
expect(this.courseInfoEdit.$modalCover.hide).toHaveBeenCalled();
|
||||
expect(model.save).not.toHaveBeenCalled();
|
||||
const previewContents = this.courseInfoEdit.$el.find('.update-contents').html();
|
||||
expect(previewContents).toEqual('existing update');
|
||||
};
|
||||
|
||||
this.testInvalidDateValue = function(value) {
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
expect(this.courseInfoEdit.$el.find('.save-button').hasClass("is-disabled")).toEqual(false);
|
||||
this.courseInfoEdit.$el.find('input.date').val(value).trigger("change");
|
||||
expect(this.courseInfoEdit.$el.find('.save-button').hasClass("is-disabled")).toEqual(true);
|
||||
this.courseInfoEdit.$el.find('input.date').val("01/01/16").trigger("change");
|
||||
expect(this.courseInfoEdit.$el.find('.save-button').hasClass("is-disabled")).toEqual(false);
|
||||
};
|
||||
|
||||
return cancelEditingUpdate = function(update, modalCover, useCancelButton) {
|
||||
if (useCancelButton) {
|
||||
return update.$el.find('.cancel-button').click();
|
||||
} else {
|
||||
return modalCover.click();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it("does send expected data on save", function() {
|
||||
const requests = AjaxHelpers["requests"](this);
|
||||
|
||||
// Create a new update, verifying that the model is created
|
||||
// in the collection and save is called.
|
||||
expect(this.collection.isEmpty()).toBeTruthy();
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
expect(this.collection.length).toEqual(1);
|
||||
const model = this.collection.at(0);
|
||||
spyOn(model, "save").and.callThrough();
|
||||
spyOn(this.courseInfoEdit.$codeMirror, 'getValue').and.returnValue('/static/image.jpg');
|
||||
|
||||
// Click the "Save button."
|
||||
this.courseInfoEdit.$el.find('.save-button').click();
|
||||
expect(model.save).toHaveBeenCalled();
|
||||
|
||||
const requestSent = JSON.parse(requests[requests.length - 1].requestBody);
|
||||
// Verify the link is not rewritten when saved.
|
||||
expect(requestSent.content).toEqual('/static/image.jpg');
|
||||
|
||||
// Verify that analytics are sent
|
||||
expect(window.analytics.track).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does rewrite links for preview", function() {
|
||||
// Create a new update.
|
||||
this.createNewUpdate('/static/image.jpg');
|
||||
|
||||
// Verify the link is rewritten for preview purposes.
|
||||
const previewContents = this.courseInfoEdit.$el.find('.update-contents').html();
|
||||
expect(previewContents).toEqual('base-asset-url/image.jpg');
|
||||
});
|
||||
|
||||
it("shows static links in edit mode", function() {
|
||||
this.createNewUpdate('/static/image.jpg');
|
||||
|
||||
// Click edit and verify CodeMirror contents.
|
||||
this.courseInfoEdit.$el.find('.edit-button').click();
|
||||
expect(this.courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg');
|
||||
});
|
||||
|
||||
it("removes newly created course info on cancel", function() {
|
||||
this.cancelNewCourseInfo(true);
|
||||
});
|
||||
|
||||
it("do not close new course info on click outside modal", function() {
|
||||
this.doNotCloseNewCourseInfo();
|
||||
});
|
||||
|
||||
it("does not remove existing course info on cancel", function() {
|
||||
this.cancelExistingCourseInfo(true);
|
||||
});
|
||||
|
||||
it("does not remove existing course info on click outside modal", function() {
|
||||
this.cancelExistingCourseInfo(false);
|
||||
});
|
||||
|
||||
it("does not allow updates to be saved with an invalid date", function() {
|
||||
this.testInvalidDateValue("Marchtober 40, 2048");
|
||||
});
|
||||
|
||||
it("does not allow updates to be saved with a blank date", function() {
|
||||
this.testInvalidDateValue("");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Course Handouts", function() {
|
||||
const handoutsTemplate = readFixtures('course_info_handouts.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures($("<script>", {id: "course_info_handouts-tpl", type: "text/template"}).text(handoutsTemplate));
|
||||
appendSetFixtures(courseInfoPage);
|
||||
|
||||
this.model = new ModuleInfo({
|
||||
id: 'handouts-id',
|
||||
data: '/static/fromServer.jpg'
|
||||
});
|
||||
|
||||
this.handoutsEdit = new CourseInfoHandoutsView({
|
||||
el: $('#course-handouts-view'),
|
||||
model: this.model,
|
||||
base_asset_url: 'base-asset-url/'
|
||||
});
|
||||
|
||||
this.handoutsEdit.render();
|
||||
});
|
||||
|
||||
it("saves <ol></ol> when content left empty", function() {
|
||||
const requests = AjaxHelpers["requests"](this);
|
||||
|
||||
// Enter empty string in the handouts section, verifying that the model
|
||||
// is saved with '<ol></ol>' instead of the empty string
|
||||
this.handoutsEdit.$el.find('.edit-button').click();
|
||||
spyOn(this.handoutsEdit.$codeMirror, 'getValue').and.returnValue('');
|
||||
spyOn(this.model, "save").and.callThrough();
|
||||
this.handoutsEdit.$el.find('.save-button').click();
|
||||
expect(this.model.save).toHaveBeenCalled();
|
||||
|
||||
const contentSaved = JSON.parse(requests[requests.length - 1].requestBody).data;
|
||||
expect(contentSaved).toEqual('<ol></ol>');
|
||||
});
|
||||
|
||||
it("does not rewrite links on save", function() {
|
||||
const requests = AjaxHelpers["requests"](this);
|
||||
|
||||
// Enter something in the handouts section, verifying that the model is saved
|
||||
// when "Save" is clicked.
|
||||
this.handoutsEdit.$el.find('.edit-button').click();
|
||||
spyOn(this.handoutsEdit.$codeMirror, 'getValue').and.returnValue('/static/image.jpg');
|
||||
spyOn(this.model, "save").and.callThrough();
|
||||
this.handoutsEdit.$el.find('.save-button').click();
|
||||
expect(this.model.save).toHaveBeenCalled();
|
||||
|
||||
const contentSaved = JSON.parse(requests[requests.length - 1].requestBody).data;
|
||||
expect(contentSaved).toEqual('/static/image.jpg');
|
||||
});
|
||||
|
||||
it("does rewrite links in initial content", function() {
|
||||
expect(this.handoutsEdit.$preview.html().trim()).toBe('base-asset-url/fromServer.jpg');
|
||||
});
|
||||
|
||||
it("does rewrite links after edit", function() {
|
||||
// Edit handouts and save.
|
||||
this.handoutsEdit.$el.find('.edit-button').click();
|
||||
spyOn(this.handoutsEdit.$codeMirror, 'getValue').and.returnValue('/static/image.jpg');
|
||||
this.handoutsEdit.$el.find('.save-button').click();
|
||||
|
||||
// Verify preview text.
|
||||
expect(this.handoutsEdit.$preview.html().trim()).toBe('base-asset-url/image.jpg');
|
||||
});
|
||||
|
||||
it("shows static links in edit mode", function() {
|
||||
// Click edit and verify CodeMirror contents.
|
||||
this.handoutsEdit.$el.find('.edit-button').click();
|
||||
expect(this.handoutsEdit.$codeMirror.getValue().trim()).toEqual('/static/fromServer.jpg');
|
||||
});
|
||||
|
||||
it("can open course handouts with bad html on edit", function() {
|
||||
// Enter some bad html in handouts section, verifying that the
|
||||
// model/handoutform opens when "Edit" is clicked
|
||||
|
||||
this.model = new ModuleInfo({
|
||||
id: 'handouts-id',
|
||||
data: '<p><a href="[URL OF FILE]>[LINK TEXT]</a></p>'
|
||||
});
|
||||
this.handoutsEdit = new CourseInfoHandoutsView({
|
||||
el: $('#course-handouts-view'),
|
||||
model: this.model,
|
||||
base_asset_url: 'base-asset-url/'
|
||||
});
|
||||
this.handoutsEdit.render();
|
||||
|
||||
expect($('.edit-handouts-form').is(':hidden')).toEqual(true);
|
||||
this.handoutsEdit.$el.find('.edit-button').click();
|
||||
expect(this.handoutsEdit.$codeMirror.getValue()).toEqual('<p><a href="[URL OF FILE]>[LINK TEXT]</a></p>');
|
||||
expect($('.edit-handouts-form').is(':hidden')).toEqual(false);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -1,32 +0,0 @@
|
||||
define(['js/views/baseview', 'js/views/course_info_update', 'js/views/course_info_handout'],
|
||||
function(BaseView, CourseInfoUpdateView, CourseInfoHandoutView) {
|
||||
/* this view should own everything on the page which has controls effecting its operation
|
||||
generate other views for the individual editors.
|
||||
The render here adds views for each update/handout by delegating to their collections but does not
|
||||
generate any html for the surrounding page.
|
||||
*/
|
||||
|
||||
var CourseInfoEdit = BaseView.extend({
|
||||
// takes CMS.Models.CourseInfo as model
|
||||
tagName: 'div',
|
||||
|
||||
render: function() {
|
||||
// instantiate the ClassInfoUpdateView and delegate the proper dom to it
|
||||
// eslint-disable-next-line no-new
|
||||
new CourseInfoUpdateView({
|
||||
el: $('body.updates'),
|
||||
collection: this.model.get('updates'),
|
||||
base_asset_url: this.model.get('base_asset_url')
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new CourseInfoHandoutView({
|
||||
el: this.$('#course-handouts-view'),
|
||||
model: this.model.get('handouts'),
|
||||
base_asset_url: this.model.get('base_asset_url')
|
||||
});
|
||||
return this;
|
||||
}
|
||||
});
|
||||
return CourseInfoEdit;
|
||||
}); // end define()
|
||||
@@ -1,78 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="base.html" />
|
||||
<%def name="online_help_token()"><% return "updates" %></%def>
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import gettext as _
|
||||
from openedx.core.djangolib.js_utils import (
|
||||
dump_js_escaped_json, js_escaped_string
|
||||
)
|
||||
%>
|
||||
|
||||
## TODO decode course # from context_course into title.
|
||||
<%block name="title">${_("Course Updates")}</%block>
|
||||
<%block name="bodyclass">is-signedin course course-info updates view-updates</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
% for template_name in ["course_info_update", "course_info_handouts"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/course_info"], function(CourseInfoFactory) {
|
||||
CourseInfoFactory(
|
||||
"${updates_url | n, js_escaped_string}",
|
||||
"${handouts_locator | n, js_escaped_string}",
|
||||
"${base_asset_url | n, js_escaped_string}"
|
||||
);
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<h1 class="page-header">
|
||||
<small class="subtitle">${_("Content")}</small>
|
||||
<span class="sr">> </span>${_("Course Updates")}
|
||||
</h1>
|
||||
|
||||
<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-update-button" disabled><span class="icon fa fa-plus" aria-hidden="true"></span> ${_('New Update')}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<div class="introduction">
|
||||
<p class="copy">${_('Use course updates to notify students of important dates or exams, highlight particular discussions in the forums, announce schedule changes, and respond to student questions. You add or edit updates in HTML.')}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="course-info-wrapper"
|
||||
% if getattr(context_course, 'language'):
|
||||
lang="${context_course.language}"
|
||||
% endif
|
||||
>
|
||||
<div class="main-column window">
|
||||
<article class="course-updates" id="course-update-view">
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
</article>
|
||||
</div>
|
||||
<div class="sidebar course-handouts" id="course-handouts-view"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -46,7 +46,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)
|
||||
updates_mfe_enabled = toggles.use_new_updates_page(context_course.id)
|
||||
video_upload_mfe_enabled = toggles.use_new_video_uploads_page(context_course.id)
|
||||
schedule_details_mfe_enabled = toggles.use_new_schedule_details_page(context_course.id)
|
||||
grading_mfe_enabled = toggles.use_new_grading_page(context_course.id)
|
||||
@@ -83,16 +82,9 @@
|
||||
<a href="${get_course_libraries_url(course_key)}">${_("Libraries")}</a>
|
||||
</li>
|
||||
% endif
|
||||
% if not updates_mfe_enabled:
|
||||
<li class="nav-item nav-course-courseware-updates">
|
||||
<a href="${course_info_url}">${_("Updates")}</a>
|
||||
</li>
|
||||
% endif
|
||||
% if updates_mfe_enabled:
|
||||
<li class="nav-item nav-course-courseware-updates">
|
||||
<a href="${get_updates_url(course_key)}">${_("Updates")}</a>
|
||||
</li>
|
||||
% endif
|
||||
% if not pages_and_resources_mfe_enabled:
|
||||
<li class="nav-item nav-course-courseware-pages">
|
||||
<a href="${tabs_url}">${_("Pages")}</a>
|
||||
|
||||
Reference in New Issue
Block a user