From c6d74f50b73d2829b5490bd1c27c615cab9ba732 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 24 Oct 2025 11:13:04 -0400 Subject: [PATCH] feat!: Drop support for the legacy textbooks page. The legacy textbooks page has been replaced with an authoring MFE equivalent. We don't need to keep the old one around. This work is part of https://github.com/openedx/edx-platform/issues/36108 BREAKING CHANGE: With this change the `legacy_studio.textbooks` waffle flag will no longer be respected and the system will behave as if the flag is always set to False. --- .../v1/serializers/course_waffle_flags.py | 11 +- .../contentstore/tests/test_contentstore.py | 12 +- .../tests/test_course_settings.py | 2 - cms/djangoapps/contentstore/toggles.py | 19 - cms/djangoapps/contentstore/utils.py | 10 +- cms/djangoapps/contentstore/views/course.py | 17 +- .../views/tests/test_textbooks.py | 9 +- cms/static/cms/js/spec/main.js | 2 - cms/static/js/collections/chapter.js | 14 - cms/static/js/collections/textbook.js | 8 - cms/static/js/factories/textbooks.js | 25 -- cms/static/js/models/chapter.js | 52 --- cms/static/js/models/textbook.js | 89 ---- cms/static/js/spec/models/textbook_spec.js | 240 ----------- cms/static/js/spec/views/textbook_spec.js | 401 ------------------ cms/static/js/spec/views/upload_spec.js | 4 +- cms/static/js/views/edit_chapter.js | 84 ---- cms/static/js/views/edit_textbook.js | 93 ---- cms/static/js/views/list_textbooks.js | 52 --- cms/static/js/views/show_textbook.js | 71 ---- cms/static/karma_cms_webpack.conf.js | 1 - cms/static/sass/_build-v1.scss | 1 - cms/static/sass/views/_textbooks.scss | 382 ----------------- cms/templates/js/edit-textbook.underscore | 28 -- cms/templates/js/no-textbooks.underscore | 3 - cms/templates/js/show-textbook.underscore | 49 --- cms/templates/textbooks.html | 104 ----- webpack-config/file-lists.js | 1 - webpack.common.config.js | 1 - 29 files changed, 33 insertions(+), 1752 deletions(-) delete mode 100644 cms/static/js/collections/chapter.js delete mode 100644 cms/static/js/collections/textbook.js delete mode 100644 cms/static/js/factories/textbooks.js delete mode 100644 cms/static/js/models/chapter.js delete mode 100644 cms/static/js/models/textbook.js delete mode 100644 cms/static/js/spec/models/textbook_spec.js delete mode 100644 cms/static/js/spec/views/textbook_spec.js delete mode 100644 cms/static/js/views/edit_chapter.js delete mode 100644 cms/static/js/views/edit_textbook.js delete mode 100644 cms/static/js/views/list_textbooks.js delete mode 100644 cms/static/js/views/show_textbook.js delete mode 100644 cms/static/sass/views/_textbooks.scss delete mode 100644 cms/templates/js/edit-textbook.underscore delete mode 100644 cms/templates/js/no-textbooks.underscore delete mode 100644 cms/templates/js/show-textbook.underscore delete mode 100644 cms/templates/textbooks.html diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py index 31cf9c36f068..5214b8efd4a6 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py @@ -148,10 +148,15 @@ def get_use_new_certificates_page(self, obj): def get_use_new_textbooks_page(self, obj): """ - Method to get the use_new_textbooks_page switch + Method to indicate whether we should use_new_textbooks_page or not. + + 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_textbooks_page(course_key) + return True def get_use_new_group_configurations_page(self, obj): """ diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index a8721d629c76..0c213f2d4fcb 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1467,6 +1467,15 @@ def test_get_html(handler): ) self.assertEqual(resp.status_code, 200) + def test_get_json(handler): + # Helper function for getting HTML for a page in Studio and + # checking that it does not error. + resp = self.client.get( + get_url(handler, course_key, 'course_key_string'), + HTTP_ACCEPT="application/json", + ) + self.assertEqual(resp.status_code, 200) + course_items = import_course_from_xml( self.store, self.user.id, TEST_DATA_DIR, ['simple'], create_if_not_present=True ) @@ -1499,8 +1508,7 @@ def test_get_html(handler): test_get_html('grading_handler') with override_waffle_flag(toggles.LEGACY_STUDIO_ADVANCED_SETTINGS, True): test_get_html('advanced_settings_handler') - with override_waffle_flag(toggles.LEGACY_STUDIO_TEXTBOOKS, True): - test_get_html('textbooks_list_handler') + test_get_json('textbooks_list_handler') # go look at the Edit page unit_key = course_key.make_usage_key('vertical', 'test_vertical') diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 9afba26b32e5..bf8007d41b41 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -171,7 +171,6 @@ def test_discussion_fields_available(self, is_pages_and_resources_enabled, @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) - @override_waffle_flag(toggles.LEGACY_STUDIO_TEXTBOOKS, True) def test_disable_advanced_settings_feature(self, disable_advanced_settings): """ If this feature is enabled, only Django Staff/Superuser should be able to access the "Advanced Settings" page. @@ -190,7 +189,6 @@ def test_disable_advanced_settings_feature(self, disable_advanced_settings): 'tabs_handler', 'settings_handler', 'grading_handler', - 'textbooks_list_handler', ): # Test that non-staff users don't see the "Advanced Settings" tab link. response = self.non_staff_client.get_html( diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py index 96a646bff211..0d0101b6c024 100644 --- a/cms/djangoapps/contentstore/toggles.py +++ b/cms/djangoapps/contentstore/toggles.py @@ -402,25 +402,6 @@ def use_new_certificates_page(course_key): return not LEGACY_STUDIO_CERTIFICATES.is_enabled(course_key) -# .. toggle_name: legacy_studio.textbooks -# .. toggle_implementation: WaffleFlag -# .. toggle_default: False -# .. toggle_description: Temporarily fall back to the old Studio Textbooks 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_TEXTBOOKS = CourseWaffleFlag('legacy_studio.textbooks', __name__) - - -def use_new_textbooks_page(course_key): - """ - Returns a boolean if new studio textbooks mfe is enabled - """ - return not LEGACY_STUDIO_TEXTBOOKS.is_enabled(course_key) - - # .. toggle_name: legacy_studio.configurations # .. toggle_implementation: WaffleFlag # .. toggle_default: False diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index a80fffec7551..58749801e3e4 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -50,7 +50,6 @@ use_new_group_configurations_page, use_new_import_page, use_new_schedule_details_page, - use_new_textbooks_page, use_new_unit_page, use_new_updates_page, use_new_video_uploads_page, @@ -492,11 +491,10 @@ def get_textbooks_url(course_locator) -> str: Gets course authoring microfrontend URL for textbooks page view. """ textbooks_url = None - if use_new_textbooks_page(course_locator): - mfe_base_url = get_course_authoring_url(course_locator) - course_mfe_url = f'{mfe_base_url}/course/{course_locator}/textbooks' - if mfe_base_url: - textbooks_url = course_mfe_url + mfe_base_url = get_course_authoring_url(course_locator) + course_mfe_url = f'{mfe_base_url}/course/{course_locator}/textbooks' + if mfe_base_url: + textbooks_url = course_mfe_url return textbooks_url diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 453e30e0aad0..f585c4b4af83 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -93,7 +93,6 @@ use_new_updates_page, use_new_advanced_settings_page, use_new_grading_page, - use_new_textbooks_page, use_new_group_configurations_page, use_new_schedule_details_page ) @@ -112,7 +111,6 @@ get_schedule_details_url, get_studio_home_url, get_updates_url, - get_textbooks_context, get_textbooks_url, initialize_permissions, remove_all_instructors, @@ -1457,17 +1455,18 @@ def textbooks_list_handler(request, course_key_string): json: overwrite all textbooks in the course with the given list """ course_key = CourseKey.from_string(course_key_string) + if "application/json" not in request.META.get('HTTP_ACCEPT', 'text/html'): + # return HTML page + # We don't need to do an access check here because + # that is done when the endpoint for the actual content of the page. + # This is just to handle redirecting anyone that has bookmarked the old + # textbooks page. + return redirect(get_textbooks_url(course_key)) + store = modulestore() with store.bulk_operations(course_key): course = get_course_and_check_access(course_key, request.user) - if "application/json" not in request.META.get('HTTP_ACCEPT', 'text/html'): - # return HTML page - if use_new_textbooks_page(course_key): - return redirect(get_textbooks_url(course_key)) - textbooks_context = get_textbooks_context(course) - return render_to_response('textbooks.html', textbooks_context) - # from here on down, we know the client has requested JSON if request.method == 'GET': return JsonResponse(course.pdf_textbooks) diff --git a/cms/djangoapps/contentstore/views/tests/test_textbooks.py b/cms/djangoapps/contentstore/views/tests/test_textbooks.py index e80c5d837910..0756d236ed42 100644 --- a/cms/djangoapps/contentstore/views/tests/test_textbooks.py +++ b/cms/djangoapps/contentstore/views/tests/test_textbooks.py @@ -4,9 +4,7 @@ import json from unittest import TestCase -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 @@ -20,15 +18,10 @@ def setUp(self): super().setUp() self.url = reverse_course_url('textbooks_list_handler', self.course.id) - @override_waffle_flag(toggles.LEGACY_STUDIO_TEXTBOOKS, True) def test_view_index(self): "Basic check that the textbook index page responds correctly" resp = self.client.get(self.url) - self.assertEqual(resp.status_code, 200) - # we don't have resp.context right now, - # due to bugs in our testing harness :( - if resp.context and resp.context.get('course'): - self.assertEqual(resp.context['course'], self.course) + self.assertEqual(resp.status_code, 302) def test_view_index_xhr(self): "Check that we get a JSON response when requested via AJAX" diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index 4e093ee573b4..3fb9ca8319bb 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -234,11 +234,9 @@ 'js/spec/models/section_spec', 'js/spec/models/settings_course_grader_spec', 'js/spec/models/settings_grading_spec', - 'js/spec/models/textbook_spec', 'js/spec/models/upload_spec', 'js/spec/views/course_info_spec', 'js/spec/views/metadata_edit_spec', - 'js/spec/views/textbook_spec', 'js/spec/views/upload_spec', 'js/spec/video/transcripts/message_manager_spec', 'js/spec/video/transcripts/utils_spec', diff --git a/cms/static/js/collections/chapter.js b/cms/static/js/collections/chapter.js deleted file mode 100644 index 2af2a85bd47e..000000000000 --- a/cms/static/js/collections/chapter.js +++ /dev/null @@ -1,14 +0,0 @@ -define(['backbone', 'js/models/chapter'], function(Backbone, ChapterModel) { - var ChapterCollection = Backbone.Collection.extend({ - model: ChapterModel, - comparator: 'order', - nextOrder: function() { - if (!this.length) { return 1; } - return this.last().get('order') + 1; - }, - isEmpty: function() { - return this.length === 0 || this.every(function(m) { return m.isEmpty(); }); - } - }); - return ChapterCollection; -}); diff --git a/cms/static/js/collections/textbook.js b/cms/static/js/collections/textbook.js deleted file mode 100644 index d388ca9800f1..000000000000 --- a/cms/static/js/collections/textbook.js +++ /dev/null @@ -1,8 +0,0 @@ -define(['backbone', 'js/models/textbook'], - function(Backbone, TextbookModel) { - var TextbookCollection = Backbone.Collection.extend({ - model: TextbookModel, - url: function() { return CMS.URL.TEXTBOOKS; } - }); - return TextbookCollection; - }); diff --git a/cms/static/js/factories/textbooks.js b/cms/static/js/factories/textbooks.js deleted file mode 100644 index c3ba710ebfda..000000000000 --- a/cms/static/js/factories/textbooks.js +++ /dev/null @@ -1,25 +0,0 @@ -import * as gettext from 'gettext'; -import * as Section from 'js/models/section'; -import * as TextbookCollection from 'js/collections/textbook'; -import * as ListTextbooksView from 'js/views/list_textbooks'; -import './base'; - -// eslint-disable-next-line no-unused-expressions -'use strict'; -export default function TextbooksFactory(textbooksJson) { - var textbooks = new TextbookCollection(textbooksJson, {parse: true}), - tbView = new ListTextbooksView({collection: textbooks}); - - $('.content-primary').append(tbView.render().el); - $('.nav-actions .new-button').click(function(event) { - tbView.addOne(event); - }); - $(window).on('beforeunload', function() { - var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); }); - if (dirty) { - return gettext('You have unsaved changes. Do you really want to leave this page?'); - } - }); -} - -export {TextbooksFactory}; diff --git a/cms/static/js/models/chapter.js b/cms/static/js/models/chapter.js deleted file mode 100644 index e241217c38aa..000000000000 --- a/cms/static/js/models/chapter.js +++ /dev/null @@ -1,52 +0,0 @@ -define(['backbone', 'gettext', 'backbone.associations'], function(Backbone, gettext) { - var Chapter = Backbone.AssociatedModel.extend({ - defaults: function() { - return { - name: '', - asset_path: '', - order: this.collection ? this.collection.nextOrder() : 1 - }; - }, - isEmpty: function() { - return !this.get('name') && !this.get('asset_path'); - }, - parse: function(response) { - if ('title' in response && !('name' in response)) { - response.name = response.title; - delete response.title; - } - if ('url' in response && !('asset_path' in response)) { - response.asset_path = response.url; - delete response.url; - } - return response; - }, - toJSON: function() { - return { - title: this.get('name'), - url: this.get('asset_path') - }; - }, - // NOTE: validation functions should return non-internationalized error - // messages. The messages will be passed through gettext in the template. - validate: function(attrs, options) { - if (!attrs.name && !attrs.asset_path) { - return { - message: gettext('Chapter name and asset_path are both required'), - attributes: {name: true, asset_path: true} - }; - } else if (!attrs.name) { - return { - message: gettext('Chapter name is required'), - attributes: {name: true} - }; - } else if (!attrs.asset_path) { - return { - message: gettext('asset_path is required'), - attributes: {asset_path: true} - }; - } - } - }); - return Chapter; -}); diff --git a/cms/static/js/models/textbook.js b/cms/static/js/models/textbook.js deleted file mode 100644 index cc14824048ec..000000000000 --- a/cms/static/js/models/textbook.js +++ /dev/null @@ -1,89 +0,0 @@ -define(['backbone', 'underscore', 'gettext', 'js/models/chapter', 'js/collections/chapter', - 'backbone.associations', 'cms/js/main'], -function(Backbone, _, gettext, ChapterModel, ChapterCollection) { - var Textbook = Backbone.AssociatedModel.extend({ - defaults: function() { - return { - name: '', - chapters: new ChapterCollection([{}]), - showChapters: false, - editing: false - }; - }, - relations: [{ - type: Backbone.Many, - key: 'chapters', - relatedModel: ChapterModel, - collectionType: ChapterCollection - }], - initialize: function() { - this.setOriginalAttributes(); - return this; - }, - setOriginalAttributes: function() { - this._originalAttributes = this.parse(this.toJSON()); - }, - reset: function() { - this.set(this._originalAttributes, {parse: true}); - }, - isDirty: function() { - return !_.isEqual(this._originalAttributes, this.parse(this.toJSON())); - }, - isEmpty: function() { - return !this.get('name') && this.get('chapters').isEmpty(); - }, - urlRoot: function() { return CMS.URL.TEXTBOOKS; }, - parse: function(response) { - var ret = $.extend(true, {}, response); - if ('tab_title' in ret && !('name' in ret)) { - ret.name = ret.tab_title; - delete ret.tab_title; - } - if ('url' in ret && !('chapters' in ret)) { - ret.chapters = {url: ret.url}; - delete ret.url; - } - _.each(ret.chapters, function(chapter, i) { - chapter.order = chapter.order || i + 1; - }); - return ret; - }, - toJSON: function() { - return { - tab_title: this.get('name'), - chapters: this.get('chapters').toJSON() - }; - }, - // NOTE: validation functions should return non-internationalized error - // messages. The messages will be passed through gettext in the template. - validate: function(attrs, options) { - if (!attrs.name) { - return { - message: gettext('Textbook name is required'), - attributes: {name: true} - }; - } - if (attrs.chapters.length === 0) { - return { - message: gettext('Please add at least one chapter'), - attributes: {chapters: true} - }; - } else { - // validate all chapters - var invalidChapters = []; - attrs.chapters.each(function(chapter) { - if (!chapter.isValid()) { - invalidChapters.push(chapter); - } - }); - if (!_.isEmpty(invalidChapters)) { - return { - message: gettext('All chapters must have a name and asset'), - attributes: {chapters: invalidChapters} - }; - } - } - } - }); - return Textbook; -}); diff --git a/cms/static/js/spec/models/textbook_spec.js b/cms/static/js/spec/models/textbook_spec.js deleted file mode 100644 index 9be93eb62468..000000000000 --- a/cms/static/js/spec/models/textbook_spec.js +++ /dev/null @@ -1,240 +0,0 @@ -define(["backbone", "js/models/textbook", "js/collections/textbook", "js/models/chapter", "js/collections/chapter", "cms/js/main"], -function(Backbone, Textbook, TextbookSet, Chapter, ChapterSet, main) { - - describe("Textbook model", function() { - beforeEach(function() { - main(); - this.model = new Textbook(); - CMS.URL.TEXTBOOKS = "/textbooks"; - }); - - afterEach(() => delete CMS.URL.TEXTBOOKS); - - describe("Basic", function() { - it("should have an empty name by default", function() { - expect(this.model.get("name")).toEqual(""); - }); - - it("should not show chapters by default", function() { - expect(this.model.get("showChapters")).toBeFalsy(); - }); - - it("should have a ChapterSet with one chapter by default", function() { - const chapters = this.model.get("chapters"); - expect(chapters).toBeInstanceOf(ChapterSet); - expect(chapters.length).toEqual(1); - expect(chapters.at(0).isEmpty()).toBeTruthy(); - }); - - it("should be empty by default", function() { - expect(this.model.isEmpty()).toBeTruthy(); - }); - - it("should have a URL root", function() { - const urlRoot = _.result(this.model, 'urlRoot'); - expect(urlRoot).toBeTruthy(); - }); - - it("should be able to reset itself", function() { - this.model.set("name", "foobar"); - this.model.reset(); - expect(this.model.get("name")).toEqual(""); - }); - - it("should not be dirty by default", function() { - expect(this.model.isDirty()).toBeFalsy(); - }); - - it("should be dirty after it's been changed", function() { - this.model.set("name", "foobar"); - expect(this.model.isDirty()).toBeTruthy(); - }); - - it("should not be dirty after calling setOriginalAttributes", function() { - this.model.set("name", "foobar"); - this.model.setOriginalAttributes(); - expect(this.model.isDirty()).toBeFalsy(); - }); - }); - - describe("Input/Output", function() { - var deepAttributes = function(obj) { - if (obj instanceof Backbone.Model) { - return deepAttributes(obj.attributes); - } else if (obj instanceof Backbone.Collection) { - return obj.map(deepAttributes); - } else if (_.isArray(obj)) { - return _.map(obj, deepAttributes); - } else if (_.isObject(obj)) { - const attributes = {}; - for (let prop of Object.keys(obj)) { - const val = obj[prop]; - attributes[prop] = deepAttributes(val); - } - return attributes; - } else { - return obj; - } - }; - - it("should match server model to client model", function() { - const serverModelSpec = { - "tab_title": "My Textbook", - "chapters": [ - {"title": "Chapter 1", "url": "/ch1.pdf"}, - {"title": "Chapter 2", "url": "/ch2.pdf"}, - ] - }; - const clientModelSpec = { - "name": "My Textbook", - "showChapters": false, - "editing": false, - "chapters": [{ - "name": "Chapter 1", - "asset_path": "/ch1.pdf", - "order": 1 - }, { - "name": "Chapter 2", - "asset_path": "/ch2.pdf", - "order": 2 - } - ] - }; - - const model = new Textbook(serverModelSpec, {parse: true}); - expect(deepAttributes(model)).toEqual(clientModelSpec); - expect(model.toJSON()).toEqual(serverModelSpec); - }); - }); - - describe("Validation", function() { - it("requires a name", function() { - const model = new Textbook({name: ""}); - expect(model.isValid()).toBeFalsy(); - }); - - it("requires at least one chapter", function() { - const model = new Textbook({name: "foo"}); - model.get("chapters").reset(); - expect(model.isValid()).toBeFalsy(); - }); - - it("requires a valid chapter", function() { - const chapter = new Chapter(); - chapter.isValid = () => false; - const model = new Textbook({name: "foo"}); - model.get("chapters").reset([chapter]); - expect(model.isValid()).toBeFalsy(); - }); - - it("requires all chapters to be valid", function() { - const chapter1 = new Chapter(); - chapter1.isValid = () => true; - const chapter2 = new Chapter(); - chapter2.isValid = () => false; - const model = new Textbook({name: "foo"}); - model.get("chapters").reset([chapter1, chapter2]); - expect(model.isValid()).toBeFalsy(); - }); - - it("can pass validation", function() { - const chapter = new Chapter(); - chapter.isValid = () => true; - const model = new Textbook({name: "foo"}); - model.get("chapters").reset([chapter]); - expect(model.isValid()).toBeTruthy(); - }); - }); - }); - - - describe("Textbook collection", function() { - beforeEach(function() { - CMS.URL.TEXTBOOKS = "/textbooks"; - this.collection = new TextbookSet(); - }); - - afterEach(() => delete CMS.URL.TEXTBOOKS); - - it("should have a url set", function() { - const url = _.result(this.collection, 'url'); - expect(url).toEqual("/textbooks"); - }); - }); - - - describe("Chapter model", function() { - beforeEach(function() { - this.model = new Chapter(); - }); - - describe("Basic", function() { - it("should have a name by default", function() { - expect(this.model.get("name")).toEqual(""); - }); - - it("should have an asset_path by default", function() { - expect(this.model.get("asset_path")).toEqual(""); - }); - - it("should have an order by default", function() { - expect(this.model.get("order")).toEqual(1); - }); - - it("should be empty by default", function() { - expect(this.model.isEmpty()).toBeTruthy(); - }); - }); - - describe("Validation", function() { - it("requires a name", function() { - const model = new Chapter({name: "", asset_path: "a.pdf"}); - expect(model.isValid()).toBeFalsy(); - }); - - it("requires an asset_path", function() { - const model = new Chapter({name: "a", asset_path: ""}); - expect(model.isValid()).toBeFalsy(); - }); - - it("can pass validation", function() { - const model = new Chapter({name: "a", asset_path: "a.pdf"}); - expect(model.isValid()).toBeTruthy(); - }); - }); - }); - - - describe("Chapter collection", function() { - beforeEach(function() { - this.collection = new ChapterSet(); - }); - - it("is empty by default", function() { - expect(this.collection.isEmpty()).toBeTruthy(); - }); - - it("is empty if all chapters are empty", function() { - this.collection.add([{}, {}, {}]); - expect(this.collection.isEmpty()).toBeTruthy(); - }); - - it("is not empty if a chapter is not empty", function() { - this.collection.add([{}, {name: "full"}, {}]); - expect(this.collection.isEmpty()).toBeFalsy(); - }); - - it("should have a nextOrder function", function() { - expect(this.collection.nextOrder()).toEqual(1); - this.collection.add([{}]); - expect(this.collection.nextOrder()).toEqual(2); - this.collection.add([{}]); - expect(this.collection.nextOrder()).toEqual(3); - // verify that it doesn't just return an incrementing value each time - expect(this.collection.nextOrder()).toEqual(3); - // try going back one - this.collection.remove(this.collection.last()); - expect(this.collection.nextOrder()).toEqual(2); - }); - }); -}); diff --git a/cms/static/js/spec/views/textbook_spec.js b/cms/static/js/spec/views/textbook_spec.js deleted file mode 100644 index 1a0332f90f0c..000000000000 --- a/cms/static/js/spec/views/textbook_spec.js +++ /dev/null @@ -1,401 +0,0 @@ -define(["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/course", - "js/collections/textbook", "js/views/show_textbook", "js/views/edit_textbook", "js/views/list_textbooks", - "js/views/edit_chapter", "common/js/components/views/feedback_prompt", - "common/js/components/views/feedback_notification", "common/js/components/utils/view_utils", - "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", - "js/spec_helpers/modal_helpers"], - function(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTextbooks, EditChapter, - Prompt, Notification, ViewUtils, AjaxHelpers, modal_helpers) { - - describe("ShowTextbook", function() { - const tpl = readFixtures('show-textbook.underscore'); - - beforeEach(function() { - setFixtures($(" -% endfor - - -<%block name="jsextra"> - - - -<%block name="page_bundle"> - <%static:webpack entry="js/factories/textbooks"> - TextbooksFactory(${textbooks | n, dump_js_escaped_json}); - - - -<%block name="content"> -
- % 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: -
-
-
-
    -
  1. - ${_("Content")} - › -
  2. -
  3. - ${_("Pages & Resources")} - › -
  4. -
-
-
-

- > ${_("Textbooks")} -

- % else: -
-

- ${_("Content")} - > ${_("Textbooks")} -

- % endif - -
-

${_("Page Actions")}

-
    -
  • - ${_("New Textbook")} -
  • -
-
-
-
- -
-
-
-
-
-

${_("Why should I break my textbook into chapters?")}

-

${_("Breaking your textbook into multiple chapters reduces loading times for students, especially those with slow Internet connections. Breaking up textbooks into chapters can also help students more easily find topic-based information.")}

-
-
-

${_("What if my book isn't divided into chapters?")}

-

${_("If your textbook doesn't have individual chapters, you can upload the entire text as a single chapter and enter a name of your choice in the Chapter Name field.")}

-
- -
- ${_("Learn more about textbooks")} -
-
-
-
- diff --git a/webpack-config/file-lists.js b/webpack-config/file-lists.js index d9e818f9127c..1ca653ba3ef2 100644 --- a/webpack-config/file-lists.js +++ b/webpack-config/file-lists.js @@ -27,7 +27,6 @@ module.exports = { path.resolve(__dirname, '../cms/static/js/views/active_video_upload_list.js'), path.resolve(__dirname, '../cms/static/js/views/assets.js'), path.resolve(__dirname, '../cms/static/js/views/course_video_settings.js'), - path.resolve(__dirname, '../cms/static/js/views/edit_chapter.js'), path.resolve(__dirname, '../cms/static/js/views/experiment_group_edit.js'), path.resolve(__dirname, '../cms/static/js/views/license.js'), path.resolve(__dirname, '../cms/static/js/views/modals/move_xblock_modal.js'), diff --git a/webpack.common.config.js b/webpack.common.config.js index ac283c3d40d8..e52a79a1bfc9 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -100,7 +100,6 @@ module.exports = Merge.merge({ // Studio Import: './cms/static/js/features/import/factories/import.js', CourseOrLibraryListing: './cms/static/js/features_jsx/studio/CourseOrLibraryListing.jsx', - 'js/factories/textbooks': './cms/static/js/factories/textbooks.js', 'js/factories/container': './cms/static/js/factories/container.js', 'js/factories/context_course': './cms/static/js/factories/context_course.js', 'js/factories/library': './cms/static/js/factories/library.js',