feat!: Drop support for the legacy textbooks page. (#37545)
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.
This commit is contained in:
@@ -148,10 +148,15 @@ class CourseWaffleFlagsSerializer(serializers.Serializer):
|
||||
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -1467,6 +1467,15 @@ class ContentStoreTest(ContentStoreTestCase):
|
||||
)
|
||||
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 @@ class ContentStoreTest(ContentStoreTestCase):
|
||||
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')
|
||||
|
||||
@@ -171,7 +171,6 @@ class CourseAdvanceSettingViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
@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 @@ class CourseAdvanceSettingViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
'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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,7 +50,6 @@ from cms.djangoapps.contentstore.toggles import (
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -93,7 +93,6 @@ from ..toggles import (
|
||||
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 @@ from ..utils import (
|
||||
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)
|
||||
|
||||
@@ -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 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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};
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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($("<script>", {id: "show-textbook-tpl", type: "text/template"}).text(tpl));
|
||||
appendSetFixtures(sandbox({id: "page-notification"}));
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}));
|
||||
this.model = new Textbook({name: "Life Sciences", id: "0life-sciences"});
|
||||
spyOn(this.model, "destroy").and.callThrough();
|
||||
this.collection = new TextbookSet([this.model]);
|
||||
this.view = new ShowTextbook({model: this.model});
|
||||
|
||||
this.promptSpies = jasmine.stealth.spyOnConstructor(Prompt, "Warning", ["show", "hide"]);
|
||||
this.promptSpies.show.and.returnValue(this.promptSpies);
|
||||
window.course = new Course({
|
||||
id: "5",
|
||||
name: "Course Name",
|
||||
url_name: "course_name",
|
||||
org: "course_org",
|
||||
num: "course_num",
|
||||
revision: "course_rev"
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.course;
|
||||
jasmine.stealth.clearSpies();
|
||||
});
|
||||
|
||||
describe("Basic", function() {
|
||||
it("should render properly", function() {
|
||||
this.view.render();
|
||||
expect(this.view.$el).toContainText("Life Sciences");
|
||||
});
|
||||
|
||||
it("should set the 'editing' property on the model when the edit button is clicked", function() {
|
||||
this.view.render().$(".edit").click();
|
||||
expect(this.model.get("editing")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should pop a delete confirmation when the delete button is clicked", function() {
|
||||
this.view.render().$(".delete").click();
|
||||
expect(this.promptSpies.constructor).toHaveBeenCalled();
|
||||
const ctorOptions = this.promptSpies.constructor.calls.mostRecent().args[0];
|
||||
expect(ctorOptions.title).toMatch(/Life Sciences/);
|
||||
// hasn't actually been removed
|
||||
expect(this.model.destroy).not.toHaveBeenCalled();
|
||||
expect(this.collection).toContain(this.model);
|
||||
});
|
||||
|
||||
it("should show chapters appropriately", function() {
|
||||
this.model.get("chapters").add([{}, {}, {}]);
|
||||
this.model.set('showChapters', false);
|
||||
this.view.render().$(".show-chapters").click();
|
||||
expect(this.model.get('showChapters')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should hide chapters appropriately", function() {
|
||||
this.model.get("chapters").add([{}, {}, {}]);
|
||||
this.model.set('showChapters', true);
|
||||
this.view.render().$(".hide-chapters").click();
|
||||
expect(this.model.get('showChapters')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("AJAX", function() {
|
||||
beforeEach(function() {
|
||||
this.savingSpies = jasmine.stealth.spyOnConstructor(Notification, "Mini",
|
||||
["show", "hide"]);
|
||||
this.savingSpies.show.and.returnValue(this.savingSpies);
|
||||
CMS.URL.TEXTBOOKS = "/textbooks";
|
||||
});
|
||||
|
||||
afterEach(() => delete CMS.URL.TEXTBOOKS);
|
||||
|
||||
it("should destroy itself on confirmation", function() {
|
||||
const requests = AjaxHelpers["requests"](this);
|
||||
|
||||
this.view.render().$(".delete").click();
|
||||
const ctorOptions = this.promptSpies.constructor.calls.mostRecent().args[0];
|
||||
// run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(this.promptSpies);
|
||||
// AJAX request has been sent, but not yet returned
|
||||
expect(this.model.destroy).toHaveBeenCalled();
|
||||
expect(requests.length).toEqual(1);
|
||||
expect(this.savingSpies.constructor).toHaveBeenCalled();
|
||||
expect(this.savingSpies.show).toHaveBeenCalled();
|
||||
expect(this.savingSpies.hide).not.toHaveBeenCalled();
|
||||
const savingOptions = this.savingSpies.constructor.calls.mostRecent().args[0];
|
||||
expect(savingOptions.title).toMatch(/Deleting/);
|
||||
// return a success response
|
||||
requests[0].respond(204);
|
||||
expect(this.savingSpies.hide).toHaveBeenCalled();
|
||||
expect(this.collection.contains(this.model)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("EditTextbook", () =>
|
||||
describe("Basic", function() {
|
||||
const tpl = readFixtures('edit-textbook.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(tpl));
|
||||
appendSetFixtures(sandbox({id: "page-notification"}));
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}));
|
||||
this.model = new Textbook({name: "Life Sciences", editing: true});
|
||||
spyOn(this.model, 'save');
|
||||
this.collection = new TextbookSet();
|
||||
this.collection.add(this.model);
|
||||
this.view = new EditTextbook({model: this.model});
|
||||
spyOn(this.view, 'render').and.callThrough();
|
||||
});
|
||||
|
||||
it("should render properly", function() {
|
||||
this.view.render();
|
||||
expect(this.view.$("input[name=textbook-name]").val()).toEqual("Life Sciences");
|
||||
});
|
||||
|
||||
it("should allow you to create new empty chapters", function() {
|
||||
this.view.render();
|
||||
const numChapters = this.model.get("chapters").length;
|
||||
this.view.$(".action-add-chapter").click();
|
||||
expect(this.model.get("chapters").length).toEqual(numChapters+1);
|
||||
expect(this.model.get("chapters").last().isEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should save properly", function() {
|
||||
this.view.render();
|
||||
this.view.$("input[name=textbook-name]").val("starfish");
|
||||
this.view.$("input[name=chapter1-name]").val("wallflower");
|
||||
this.view.$("input[name=chapter1-asset-path]").val("foobar");
|
||||
this.view.$("form").submit();
|
||||
expect(this.model.get("name")).toEqual("starfish");
|
||||
const chapter = this.model.get("chapters").first();
|
||||
expect(chapter.get("name")).toEqual("wallflower");
|
||||
expect(chapter.get("asset_path")).toEqual("foobar");
|
||||
expect(this.model.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not save on invalid", function() {
|
||||
this.view.render();
|
||||
this.view.$("input[name=textbook-name]").val("");
|
||||
this.view.$("input[name=chapter1-asset-path]").val("foobar.pdf");
|
||||
this.view.$("form").submit();
|
||||
expect(this.model.validationError).toBeTruthy();
|
||||
expect(this.model.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not save on cancel", function() {
|
||||
this.model.get("chapters").add([{name: "a", asset_path: "b"}]);
|
||||
this.view.render();
|
||||
this.view.$("input[name=textbook-name]").val("starfish");
|
||||
this.view.$("input[name=chapter1-asset-path]").val("foobar.pdf");
|
||||
this.view.$(".action-cancel").click();
|
||||
expect(this.model.get("name")).not.toEqual("starfish");
|
||||
const chapter = this.model.get("chapters").first();
|
||||
expect(chapter.get("asset_path")).not.toEqual("foobar");
|
||||
expect(this.model.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not re-render on cancel", function() {
|
||||
this.view.render();
|
||||
this.view.$(".action-cancel").click();
|
||||
expect(this.view.render.calls.count()).toEqual(1);
|
||||
});
|
||||
|
||||
it("should be possible to correct validation errors", function() {
|
||||
this.view.render();
|
||||
this.view.$("input[name=textbook-name]").val("");
|
||||
this.view.$("input[name=chapter1-asset-path]").val("foobar.pdf");
|
||||
this.view.$("form").submit();
|
||||
expect(this.model.validationError).toBeTruthy();
|
||||
expect(this.model.save).not.toHaveBeenCalled();
|
||||
this.view.$("input[name=textbook-name]").val("starfish");
|
||||
this.view.$("input[name=chapter1-name]").val("foobar");
|
||||
this.view.$("form").submit();
|
||||
expect(this.model.validationError).toBeFalsy();
|
||||
expect(this.model.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes all empty chapters on cancel if the model has a non-empty chapter", function() {
|
||||
const chapters = this.model.get("chapters");
|
||||
chapters.at(0).set("name", "non-empty");
|
||||
this.model.setOriginalAttributes();
|
||||
this.view.render();
|
||||
chapters.add([{}, {}, {}]); // add three empty chapters
|
||||
expect(chapters.length).toEqual(4);
|
||||
this.view.$(".action-cancel").click();
|
||||
expect(chapters.length).toEqual(1);
|
||||
expect(chapters.first().get('name')).toEqual("non-empty");
|
||||
});
|
||||
|
||||
it("removes all empty chapters on cancel except one if the model has no non-empty chapters", function() {
|
||||
const chapters = this.model.get("chapters");
|
||||
this.view.render();
|
||||
chapters.add([{}, {}, {}]); // add three empty chapters
|
||||
expect(chapters.length).toEqual(4);
|
||||
this.view.$(".action-cancel").click();
|
||||
expect(chapters.length).toEqual(1);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
describe("ListTextbooks", function() {
|
||||
const noTextbooksTpl = readFixtures("no-textbooks.underscore");
|
||||
const editTextbooktpl = readFixtures('edit-textbook.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
appendSetFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl));
|
||||
appendSetFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(editTextbooktpl));
|
||||
this.collection = new TextbookSet;
|
||||
this.view = new ListTextbooks({collection: this.collection});
|
||||
this.view.render();
|
||||
});
|
||||
|
||||
it("should scroll to newly added textbook", function() {
|
||||
spyOn(ViewUtils, 'setScrollOffset');
|
||||
this.view.$(".new-button").click();
|
||||
const $sectionEl = this.view.$el.find('section:last');
|
||||
expect($sectionEl.length).toEqual(1);
|
||||
expect(ViewUtils.setScrollOffset).toHaveBeenCalledWith($sectionEl, 0);
|
||||
});
|
||||
|
||||
it("should focus first input element of newly added textbook", function() {
|
||||
spyOn(jQuery.fn, 'focus').and.callThrough();
|
||||
jasmine.addMatchers({
|
||||
toHaveBeenCalledOnJQueryObject() {
|
||||
return {
|
||||
compare(actual, expected) {
|
||||
return {
|
||||
pass: actual.calls && actual.calls.mostRecent() &&
|
||||
(actual.calls.mostRecent().object[0] === expected[0])
|
||||
};
|
||||
}
|
||||
};
|
||||
}});
|
||||
this.view.$(".new-button").click();
|
||||
const $inputEl = this.view.$el.find('section:last input:first');
|
||||
expect($inputEl.length).toEqual(1);
|
||||
// testing for element focused seems to be tricky
|
||||
// (see http://stackoverflow.com/questions/967096)
|
||||
// and the following doesn't seem to work
|
||||
// expect($inputEl).toBeFocused()
|
||||
// expect($inputEl.find(':focus').length).toEqual(1)
|
||||
expect(jQuery.fn.focus).toHaveBeenCalledOnJQueryObject($inputEl);
|
||||
});
|
||||
|
||||
it("should re-render when new textbook added", function() {
|
||||
spyOn(this.view, 'render').and.callThrough();
|
||||
this.view.$(".new-button").click();
|
||||
expect(this.view.render.calls.count()).toEqual(1);
|
||||
});
|
||||
|
||||
it("should remove textbook html section on model.destroy", function() {
|
||||
this.model = new Textbook({name: "Life Sciences", id: "0life-sciences"});
|
||||
this.collection.add(this.model);
|
||||
this.view.render();
|
||||
CMS.URL.TEXTBOOKS = "/textbooks"; // for AJAX
|
||||
expect(this.view.$el.find('section').length).toEqual(1);
|
||||
this.model.destroy();
|
||||
expect(this.view.$el.find('section').length).toEqual(0);
|
||||
delete CMS.URL.TEXTBOOKS;
|
||||
});
|
||||
});
|
||||
|
||||
// describe "ListTextbooks", ->
|
||||
// noTextbooksTpl = readFixtures("no-textbooks.underscore")
|
||||
//
|
||||
// beforeEach ->
|
||||
// setFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl))
|
||||
// @showSpies = spyOnConstructor("ShowTextbook", ["render"])
|
||||
// @showSpies.render.and.returnValue(@showSpies) # equivalent of `return this`
|
||||
// showEl = $("<li>")
|
||||
// @showSpies.$el = showEl
|
||||
// @showSpies.el = showEl.get(0)
|
||||
// @editSpies = spyOnConstructor("EditTextbook", ["render"])
|
||||
// editEl = $("<li>")
|
||||
// @editSpies.render.and.returnValue(@editSpies)
|
||||
// @editSpies.$el = editEl
|
||||
// @editSpies.el= editEl.get(0)
|
||||
//
|
||||
// @collection = new TextbookSet
|
||||
// @view = new ListTextbooks({collection: @collection})
|
||||
// @view.render()
|
||||
//
|
||||
// it "should render the empty template if there are no textbooks", ->
|
||||
// expect(@view.$el).toContainText("You haven't added any textbooks to this course yet")
|
||||
// expect(@view.$el).toContain(".new-button")
|
||||
// expect(@showSpies.constructor).not.toHaveBeenCalled()
|
||||
// expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
//
|
||||
// it "should render ShowTextbook views by default if no textbook is being edited", ->
|
||||
// # add three empty textbooks to the collection
|
||||
// @collection.add([{}, {}, {}])
|
||||
// # reset spies due to re-rendering on collection modification
|
||||
// @showSpies.constructor.reset()
|
||||
// @editSpies.constructor.reset()
|
||||
// # render once and test
|
||||
// @view.render()
|
||||
//
|
||||
// expect(@view.$el).not.toContainText(
|
||||
// "You haven't added any textbooks to this course yet")
|
||||
// expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
// expect(@showSpies.constructor.calls.length).toEqual(3);
|
||||
// expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
//
|
||||
// it "should render an EditTextbook view for a textbook being edited", ->
|
||||
// # add three empty textbooks to the collection: the first and third
|
||||
// # should be shown, and the second should be edited
|
||||
// @collection.add([{editing: false}, {editing: true}, {editing: false}])
|
||||
// editing = @collection.at(1)
|
||||
// expect(editing.get("editing")).toBeTruthy()
|
||||
// # reset spies
|
||||
// @showSpies.constructor.reset()
|
||||
// @editSpies.constructor.reset()
|
||||
// # render once and test
|
||||
// @view.render()
|
||||
//
|
||||
// expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
// expect(@showSpies.constructor.calls.length).toEqual(2)
|
||||
// expect(@showSpies.constructor).not.toHaveBeenCalledWith({model: editing})
|
||||
// expect(@editSpies.constructor).toHaveBeenCalled()
|
||||
// expect(@editSpies.constructor.calls.length).toEqual(1)
|
||||
// expect(@editSpies.constructor).toHaveBeenCalledWith({model: editing})
|
||||
//
|
||||
// it "should add a new textbook when the new-button is clicked", ->
|
||||
// # reset spies
|
||||
// @showSpies.constructor.reset()
|
||||
// @editSpies.constructor.reset()
|
||||
// # test
|
||||
// @view.$(".new-button").click()
|
||||
//
|
||||
// expect(@collection.length).toEqual(1)
|
||||
// expect(@view.$el).toContain(@editSpies.$el)
|
||||
// expect(@view.$el).not.toContain(@showSpies.$el)
|
||||
|
||||
|
||||
describe("EditChapter", function() {
|
||||
beforeEach(function() {
|
||||
modal_helpers.installModalTemplates();
|
||||
this.model = new Chapter({
|
||||
name: "Chapter 1",
|
||||
asset_path: "/ch1.pdf"
|
||||
});
|
||||
this.collection = new ChapterSet();
|
||||
this.collection.add(this.model);
|
||||
this.view = new EditChapter({model: this.model});
|
||||
spyOn(this.view, "remove").and.callThrough();
|
||||
CMS.URL.UPLOAD_ASSET = "/upload";
|
||||
window.course = new Course({name: "abcde"});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
delete CMS.URL.UPLOAD_ASSET;
|
||||
delete window.course;
|
||||
});
|
||||
|
||||
it("can render", function() {
|
||||
this.view.render();
|
||||
expect(this.view.$("input.chapter-name").val()).toEqual("Chapter 1");
|
||||
expect(this.view.$("input.chapter-asset-path").val()).toEqual("/ch1.pdf");
|
||||
});
|
||||
|
||||
it("can delete itself", function() {
|
||||
this.view.render().$(".action-close").click();
|
||||
expect(this.collection.length).toEqual(0);
|
||||
expect(this.view.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// it "can open an upload dialog", ->
|
||||
// uploadSpies = spyOnConstructor("UploadDialog", ["show", "el"])
|
||||
// uploadSpies.show.and.returnValue(uploadSpies)
|
||||
//
|
||||
// @view.render().$(".action-upload").click()
|
||||
// ctorOptions = uploadSpies.constructor.calls.mostRecent().args[0]
|
||||
// expect(ctorOptions.model.get('title')).toMatch(/abcde/)
|
||||
// expect(typeof ctorOptions.onSuccess).toBe('function')
|
||||
// expect(uploadSpies.show).toHaveBeenCalled()
|
||||
|
||||
// Disabling because this test does not close the modal dialog. This can cause
|
||||
// tests that run after it to fail (see STUD-1963).
|
||||
xit("saves content when opening upload dialog", function() {
|
||||
this.view.render();
|
||||
this.view.$("input.chapter-name").val("rainbows");
|
||||
this.view.$("input.chapter-asset-path").val("unicorns");
|
||||
this.view.$(".action-upload").click();
|
||||
expect(this.model.get("name")).toEqual("rainbows");
|
||||
expect(this.model.get("asset_path")).toEqual("unicorns");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
define(["underscore", "sinon", "js/models/uploads", "js/views/uploads", "js/models/chapter",
|
||||
define(["underscore", "sinon", "js/models/uploads", "js/views/uploads",
|
||||
"edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/spec_helpers/modal_helpers"],
|
||||
(_, sinon, FileUpload, UploadDialog, Chapter, AjaxHelpers, modal_helpers) =>
|
||||
(_, sinon, FileUpload, UploadDialog, AjaxHelpers, modal_helpers) =>
|
||||
|
||||
describe("UploadDialog", function() {
|
||||
const tpl = readFixtures("upload-dialog.underscore"),
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/* global course */
|
||||
|
||||
define(['underscore', 'jquery', 'gettext', 'edx-ui-toolkit/js/utils/html-utils',
|
||||
'js/views/baseview', 'js/models/uploads', 'js/views/uploads', 'text!templates/edit-chapter.underscore'],
|
||||
function(_, $, gettext, HtmlUtils, BaseView, FileUploadModel, UploadDialogView, editChapterTemplate) {
|
||||
'use strict';
|
||||
|
||||
var EditChapter = BaseView.extend({
|
||||
initialize: function() {
|
||||
this.template = HtmlUtils.template(editChapterTemplate);
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
tagName: 'li',
|
||||
className: function() {
|
||||
return 'field-group chapter chapter' + this.model.get('order');
|
||||
},
|
||||
render: function() {
|
||||
HtmlUtils.setHtml(
|
||||
this.$el,
|
||||
this.template({
|
||||
name: this.model.get('name'),
|
||||
asset_path: this.model.get('asset_path'),
|
||||
order: this.model.get('order'),
|
||||
error: this.model.validationError
|
||||
})
|
||||
);
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
'change .chapter-name': 'changeName',
|
||||
'change .chapter-asset-path': 'changeAssetPath',
|
||||
'click .action-close': 'removeChapter',
|
||||
'click .action-upload': 'openUploadDialog',
|
||||
submit: 'uploadAsset'
|
||||
},
|
||||
changeName: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set({
|
||||
name: this.$('.chapter-name').val()
|
||||
}, {silent: true});
|
||||
return this;
|
||||
},
|
||||
changeAssetPath: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set({
|
||||
asset_path: this.$('.chapter-asset-path').val()
|
||||
}, {silent: true});
|
||||
return this;
|
||||
},
|
||||
removeChapter: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.collection.remove(this.model);
|
||||
return this.remove();
|
||||
},
|
||||
openUploadDialog: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set({
|
||||
name: this.$('input.chapter-name').val(),
|
||||
asset_path: this.$('input.chapter-asset-path').val()
|
||||
});
|
||||
var msg = new FileUploadModel({
|
||||
title: _.template(gettext('Upload a new PDF to “<%- name %>”'))(
|
||||
{name: course.get('name')}),
|
||||
message: gettext('Please select a PDF file to upload.'),
|
||||
mimeTypes: ['application/pdf']
|
||||
});
|
||||
var that = this;
|
||||
var view = new UploadDialogView({
|
||||
model: msg,
|
||||
onSuccess: function(response) {
|
||||
var options = {};
|
||||
if (!that.model.get('name')) {
|
||||
options.name = response.asset.displayname;
|
||||
}
|
||||
options.asset_path = response.asset.portable_url;
|
||||
that.model.set(options);
|
||||
}
|
||||
});
|
||||
view.show();
|
||||
}
|
||||
});
|
||||
|
||||
return EditChapter;
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
define(['js/views/baseview', 'underscore', 'jquery', 'js/views/edit_chapter', 'common/js/components/views/feedback_notification'],
|
||||
function(BaseView, _, $, EditChapterView, NotificationView) {
|
||||
var EditTextbook = BaseView.extend({
|
||||
initialize: function() {
|
||||
this.template = this.loadTemplate('edit-textbook');
|
||||
this.listenTo(this.model, 'invalid', this.render);
|
||||
var chapters = this.model.get('chapters');
|
||||
this.listenTo(chapters, 'add', this.addOne);
|
||||
this.listenTo(chapters, 'reset', this.addAll);
|
||||
},
|
||||
tagName: 'section',
|
||||
className: 'textbook',
|
||||
render: function() {
|
||||
this.$el.html(this.template({ // xss-lint: disable=javascript-jquery-html
|
||||
name: this.model.get('name'),
|
||||
error: this.model.validationError
|
||||
}));
|
||||
this.addAll();
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
'change input[name=textbook-name]': 'setName',
|
||||
submit: 'setAndClose',
|
||||
'click .action-cancel': 'cancel',
|
||||
'click .action-add-chapter': 'createChapter'
|
||||
},
|
||||
addOne: function(chapter) {
|
||||
var view = new EditChapterView({model: chapter});
|
||||
this.$('ol.chapters').append(view.render().el);
|
||||
return this;
|
||||
},
|
||||
addAll: function() {
|
||||
this.model.get('chapters').each(this.addOne, this);
|
||||
},
|
||||
createChapter: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.setValues();
|
||||
this.model.get('chapters').add([{}]);
|
||||
},
|
||||
setName: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set('name', this.$('#textbook-name-input').val(), {silent: true});
|
||||
},
|
||||
setValues: function() {
|
||||
this.setName();
|
||||
var that = this;
|
||||
_.each(this.$('li'), function(li, i) {
|
||||
var chapter = that.model.get('chapters').at(i);
|
||||
if (!chapter) { return; }
|
||||
chapter.set({
|
||||
name: $('.chapter-name', li).val(),
|
||||
asset_path: $('.chapter-asset-path', li).val()
|
||||
});
|
||||
});
|
||||
return this;
|
||||
},
|
||||
setAndClose: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.setValues();
|
||||
if (!this.model.isValid()) { return; }
|
||||
var saving = new NotificationView.Mini({
|
||||
title: gettext('Saving')
|
||||
}).show();
|
||||
var that = this;
|
||||
this.model.save({}, {
|
||||
success: function() {
|
||||
that.model.setOriginalAttributes();
|
||||
that.close();
|
||||
},
|
||||
complete: function() {
|
||||
saving.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
cancel: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.reset();
|
||||
return this.close();
|
||||
},
|
||||
close: function() {
|
||||
var textbooks = this.model.collection;
|
||||
this.remove();
|
||||
if (this.model.isNew()) {
|
||||
// if the textbook has never been saved, remove it
|
||||
textbooks.remove(this.model);
|
||||
}
|
||||
// don't forget to tell the model that it's no longer being edited
|
||||
this.model.set('editing', false);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
return EditTextbook;
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
define(['js/views/baseview', 'jquery', 'js/views/edit_textbook', 'js/views/show_textbook', 'common/js/components/utils/view_utils'],
|
||||
function(BaseView, $, EditTextbookView, ShowTextbookView, ViewUtils) {
|
||||
var ListTextbooks = BaseView.extend({
|
||||
initialize: function() {
|
||||
this.emptyTemplate = this.loadTemplate('no-textbooks');
|
||||
this.listenTo(this.collection, 'change:editing', this.render);
|
||||
this.listenTo(this.collection, 'destroy', this.handleDestroy);
|
||||
},
|
||||
tagName: 'div',
|
||||
className: 'textbooks-list',
|
||||
render: function() {
|
||||
var textbooks = this.collection;
|
||||
if (textbooks.length === 0) {
|
||||
this.$el.html(this.emptyTemplate()); // xss-lint: disable=javascript-jquery-html
|
||||
} else {
|
||||
this.$el.empty();
|
||||
var that = this;
|
||||
textbooks.each(function(textbook) {
|
||||
var view;
|
||||
if (textbook.get('editing')) {
|
||||
view = new EditTextbookView({model: textbook});
|
||||
} else {
|
||||
view = new ShowTextbookView({model: textbook});
|
||||
}
|
||||
that.$el.append(view.render().el);
|
||||
});
|
||||
}
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
'click .new-button': 'addOne'
|
||||
},
|
||||
addOne: function(e) {
|
||||
var $sectionEl, $inputEl;
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.collection.add([{editing: true}]); // (render() call triggered here)
|
||||
this.render();
|
||||
// find the outer 'section' tag for the newly added textbook
|
||||
$sectionEl = this.$el.find('section:last');
|
||||
// scroll to put this at top of viewport
|
||||
ViewUtils.setScrollOffset($sectionEl, 0);
|
||||
// find the first input element in this section
|
||||
$inputEl = $sectionEl.find('input:first');
|
||||
// activate the text box (so user can go ahead and start typing straight away)
|
||||
$inputEl.focus().select();
|
||||
},
|
||||
handleDestroy: function(model, collection, options) {
|
||||
collection.remove(model);
|
||||
}
|
||||
});
|
||||
return ListTextbooks;
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
define(['js/views/baseview', 'underscore', 'gettext', 'common/js/components/views/feedback_notification',
|
||||
'common/js/components/views/feedback_prompt'],
|
||||
function(BaseView, _, gettext, NotificationView, PromptView) {
|
||||
var ShowTextbook = BaseView.extend({
|
||||
initialize: function() {
|
||||
this.template = _.template($('#show-textbook-tpl').text());
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.listenTo(this.model, 'destroy', this.remove);
|
||||
},
|
||||
tagName: 'section',
|
||||
className: 'textbook',
|
||||
events: {
|
||||
'click .edit': 'editTextbook',
|
||||
'click .delete': 'confirmDelete',
|
||||
'click .show-chapters': 'showChapters',
|
||||
'click .hide-chapters': 'hideChapters'
|
||||
},
|
||||
render: function() {
|
||||
var attrs = $.extend({}, this.model.attributes);
|
||||
attrs.bookindex = this.model.collection.indexOf(this.model);
|
||||
attrs.course = window.course.attributes;
|
||||
this.$el.html(this.template(attrs)); // xss-lint: disable=javascript-jquery-html
|
||||
return this;
|
||||
},
|
||||
editTextbook: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set('editing', true);
|
||||
},
|
||||
confirmDelete: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
var textbook = this.model;
|
||||
new PromptView.Warning({
|
||||
title: _.template(gettext('Delete “<%- name %>”?'))(
|
||||
{name: textbook.get('name')}
|
||||
),
|
||||
message: gettext("Deleting a textbook cannot be undone and once deleted any reference to it in your courseware's navigation will also be removed."),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Delete'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
var delmsg = new NotificationView.Mini({
|
||||
title: gettext('Deleting')
|
||||
}).show();
|
||||
textbook.destroy({
|
||||
complete: function() {
|
||||
delmsg.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Cancel'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).show();
|
||||
},
|
||||
showChapters: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set('showChapters', true);
|
||||
},
|
||||
hideChapters: function(e) {
|
||||
if (e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set('showChapters', false);
|
||||
}
|
||||
});
|
||||
return ShowTextbook;
|
||||
});
|
||||
@@ -27,7 +27,6 @@ var options = {
|
||||
// {pattern: 'js/factories/context_course.js', webpack: true},
|
||||
// {pattern: 'js/factories/edit_tabs.js', webpack: true},
|
||||
// {pattern: 'js/factories/library.js', webpack: true},
|
||||
// {pattern: 'js/factories/textbooks.js', webpack: true},
|
||||
// ],
|
||||
|
||||
// All spec files should be imported in main_webpack.js, rather than being listed here
|
||||
|
||||
@@ -72,7 +72,6 @@
|
||||
@import 'views/static-pages';
|
||||
@import 'views/container';
|
||||
@import 'views/users';
|
||||
@import 'views/textbooks';
|
||||
@import 'views/export-git';
|
||||
@import 'views/group-configuration';
|
||||
@import 'views/video-upload';
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
// studio - views - textbooks
|
||||
// ====================
|
||||
|
||||
.view-textbooks {
|
||||
.content-primary,
|
||||
.content-supplementary {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.content-primary {
|
||||
@extend .ui-col-wide;
|
||||
|
||||
.no-textbook-content {
|
||||
@extend %no-content;
|
||||
|
||||
color: $gray-d1;
|
||||
}
|
||||
|
||||
.textbook {
|
||||
@extend %ui-window;
|
||||
|
||||
position: relative;
|
||||
|
||||
.view-textbook {
|
||||
padding: $baseline ($baseline*1.5);
|
||||
|
||||
header {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.textbook-title {
|
||||
@extend %t-title4;
|
||||
@extend %t-strong;
|
||||
|
||||
@include margin-right($baseline*14);
|
||||
}
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@include transition(rotate 0.15s ease-in-out 0.25s);
|
||||
|
||||
@extend %t-action1;
|
||||
|
||||
display: inline-block;
|
||||
width: ($baseline*0.75);
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
&.is-selectable {
|
||||
@extend %ui-fake-link;
|
||||
|
||||
&:hover {
|
||||
color: theme-color("primary");
|
||||
|
||||
.ui-toggle-expansion {
|
||||
color: theme-color("primary");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chapters {
|
||||
margin-left: $baseline;
|
||||
|
||||
.chapter {
|
||||
@extend %t-copy-sub2;
|
||||
|
||||
margin-bottom: ($baseline/4);
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
|
||||
.chapter-name {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 45%;
|
||||
margin-right: ($baseline/2);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chapter-asset-path {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
color: $gray-l1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
@include transition(opacity 0.15s 0.25s ease-in-out);
|
||||
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: $baseline;
|
||||
|
||||
@include right($baseline);
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/4);
|
||||
|
||||
.view {
|
||||
@include blue-button;
|
||||
|
||||
@extend %t-action4;
|
||||
}
|
||||
|
||||
.edit {
|
||||
@include blue-button;
|
||||
|
||||
@extend %t-action4;
|
||||
}
|
||||
|
||||
.delete {
|
||||
@extend %ui-btn-non;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.edit-textbook {
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
background: $white;
|
||||
|
||||
.wrapper-form {
|
||||
padding: $baseline ($baseline*1.5);
|
||||
}
|
||||
|
||||
|
||||
fieldset {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
.actions {
|
||||
box-shadow: inset 0 1px 2px $shadow;
|
||||
border-top: 1px solid $gray-l1;
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
background: $gray-l6;
|
||||
|
||||
.action {
|
||||
margin-right: ($baseline/4);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// add a chapter is below with chapters styling
|
||||
|
||||
.action-primary {
|
||||
@include blue-button;
|
||||
@include transition(all 0.15s);
|
||||
|
||||
@extend %t-action2;
|
||||
@extend %t-strong;
|
||||
|
||||
display: inline-block;
|
||||
padding: ($baseline/5) $baseline;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include grey-button;
|
||||
@include transition(all 0.15s);
|
||||
|
||||
@extend %t-action2;
|
||||
@extend %t-strong;
|
||||
|
||||
display: inline-block;
|
||||
padding: ($baseline/5) $baseline;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend %t-copy-sub2;
|
||||
|
||||
margin: ($baseline) 0 ($baseline/2) 0;
|
||||
color: $gray;
|
||||
|
||||
strong {
|
||||
@extend %t-strong;
|
||||
}
|
||||
}
|
||||
|
||||
.chapters-fields,
|
||||
.textbook-fields {
|
||||
@extend %cont-no-list;
|
||||
|
||||
.field {
|
||||
margin: 0 0 ($baseline*0.75) 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.required {
|
||||
label {
|
||||
@extend %t-strong;
|
||||
}
|
||||
|
||||
label::after {
|
||||
margin-left: ($baseline/4);
|
||||
content: "*";
|
||||
}
|
||||
}
|
||||
|
||||
label,
|
||||
input,
|
||||
textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
@extend %t-copy-sub1;
|
||||
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
|
||||
&.is-focused {
|
||||
color: theme-color("primary");
|
||||
}
|
||||
}
|
||||
|
||||
&.add-textbook-name label {
|
||||
@extend %t-title5;
|
||||
}
|
||||
|
||||
|
||||
//this section is borrowed from _account.scss - we should clean up and unify later
|
||||
input,
|
||||
textarea {
|
||||
@extend %t-copy-base;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: ($baseline/2);
|
||||
|
||||
&.long {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.short {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
color: $gray-l4;
|
||||
}
|
||||
|
||||
:-moz-placeholder {
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
:-ms-input-placeholder {
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
+ .tip {
|
||||
color: $gray-d1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea.long {
|
||||
height: ($baseline*5);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/4);
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
& + label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
@extend %t-copy-sub2;
|
||||
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
|
||||
display: block;
|
||||
margin-top: ($baseline/4);
|
||||
color: $gray-d1;
|
||||
}
|
||||
|
||||
&.error {
|
||||
label {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-group {
|
||||
@include clearfix();
|
||||
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
|
||||
.field {
|
||||
display: block;
|
||||
width: 46%;
|
||||
border-bottom: none;
|
||||
|
||||
@include margin(0, ($baseline*0.75), 0, 0);
|
||||
|
||||
padding: ($baseline/4) 0 0 0;
|
||||
|
||||
@include float(left);
|
||||
|
||||
position: relative;
|
||||
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-upload {
|
||||
@extend %ui-btn-flat-outline;
|
||||
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
|
||||
@include right(0);
|
||||
}
|
||||
}
|
||||
|
||||
.action-close {
|
||||
@include transition(color $tmg-f2 ease-in-out);
|
||||
|
||||
@extend %t-action1;
|
||||
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-top: ($baseline*2);
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: $blue-l3;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.action-add-chapter {
|
||||
@extend %ui-btn-flat-outline;
|
||||
@extend %t-action1;
|
||||
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: ($baseline*1.5) 0 0 0;
|
||||
padding: ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-supplementary {
|
||||
@extend .ui-col-narrow;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<form class="edit-textbook" id="edit_textbook_form">
|
||||
<div class="wrapper-form">
|
||||
<% if (error && error.message) { %>
|
||||
<div id="edit_textbook_error" class="message message-status message-status error is-shown" name="edit_textbook_error">
|
||||
<%- gettext(error.message) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<fieldset class="textbook-fields">
|
||||
<legend class="sr"><%- gettext("Textbook information") %></legend>
|
||||
<div class="input-wrap field text required add-textbook-name <% if(error && error.attributes && error.attributes.name) { print('error'); } %>">
|
||||
<label for="textbook-name-input"><%- gettext("Textbook Name") %></label>
|
||||
<input id="textbook-name-input" name="textbook-name" type="text" placeholder="<%- gettext("Introduction to Cookie Baking") %>" value="<%- name %>">
|
||||
<span class="tip tip-stacked"><%- gettext("provide the title/name of the text book as you would like your students to see it") %></span>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="chapters-fields">
|
||||
<legend class="sr"><%- gettext("Chapter information") %></legend>
|
||||
<ol class="chapters list-input enum"></ol>
|
||||
|
||||
<button class="action action-add-chapter"><span class="icon fa fa-plus" aria-hidden="true"></span> <%- gettext("Add a Chapter") %></button>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="action action-primary" type="submit"><%- gettext("Save") %></button>
|
||||
<button class="action action-secondary action-cancel"><%- gettext("Cancel") %></button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,3 +0,0 @@
|
||||
<div class="no-textbook-content">
|
||||
<p><%- gettext("You haven't added any textbooks to this course yet.") %><a href="#" class="button new-button"><span class="icon fa fa-plus" aria-hidden="true"></span><%- gettext("Add your first textbook") %></a></p>
|
||||
</div>
|
||||
@@ -1,49 +0,0 @@
|
||||
<div class="view-textbook">
|
||||
|
||||
<div class="wrap-textbook">
|
||||
<header>
|
||||
<h3 class="textbook-title"><%- name %></h3>
|
||||
</header>
|
||||
|
||||
<% if(chapters.length > 1) {%>
|
||||
<p><a href="#" class="chapter-toggle
|
||||
<% if(showChapters){ print('hide'); } else { print('show'); } %>-chapters">
|
||||
<span class="ui-toggle-expansion icon fa fa-caret-<% if(showChapters){ print('down'); } else { print('right'); } %>" aria-hidden="true"></span>
|
||||
<%- chapters.length %> <%- gettext('PDF Chapters') %>
|
||||
</a></p>
|
||||
<% } else if(chapters.length === 1) { %>
|
||||
<p dir="ltr">
|
||||
<%- chapters.at(0).get("asset_path") %>
|
||||
</p>
|
||||
<% } %>
|
||||
|
||||
|
||||
<% if(showChapters) { %>
|
||||
<ol class="chapters">
|
||||
<% chapters.each(function(chapter) { %>
|
||||
<li class="chapter">
|
||||
<span class="chapter-name"><%- chapter.get('name') %></span>
|
||||
<span class="chapter-asset-path"><%- chapter.get('asset_path') %></span>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ol>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<ul class="actions textbook-actions">
|
||||
<li class="action action-view">
|
||||
<a href="//<%- CMS.URL.LMS_BASE %>/courses/<%- course.id %>/pdfbook/<%- bookindex %>/" class="view"><%- gettext("View Live") %></a>
|
||||
</li>
|
||||
<li class="action action-edit">
|
||||
<button class="edit"><%- gettext("Edit") %></button>
|
||||
</li>
|
||||
<li class="action action-delete">
|
||||
<button class="delete action-icon" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true" ></span></button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
@@ -1,104 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="base.html" />
|
||||
<%def name="online_help_token()"><% return "textbooks" %></%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
|
||||
from cms.djangoapps.contentstore.utils import get_pages_and_resources_url
|
||||
%>
|
||||
|
||||
<%block name="title">${_("Textbooks")}</%block>
|
||||
<%block name="bodyclass">is-signedin course view-textbooks</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["edit-textbook", "show-textbook", "no-textbooks", "basic-modal", "modal-button", "upload-dialog"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
window.CMS = window.CMS || {};
|
||||
CMS.URL = CMS.URL || {};
|
||||
CMS.URL.UPLOAD_ASSET = "${upload_asset_url | n, js_escaped_string}"
|
||||
CMS.URL.TEXTBOOKS = "${textbook_url | n, js_escaped_string}"
|
||||
CMS.URL.LMS_BASE = "${settings.LMS_BASE | n, js_escaped_string}"
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="page_bundle">
|
||||
<%static:webpack entry="js/factories/textbooks">
|
||||
TextbooksFactory(${textbooks | n, dump_js_escaped_json});
|
||||
</%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="hyperlink" 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>${_("Textbooks")}
|
||||
</h1>
|
||||
% else:
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<h1 class="page-header">
|
||||
<small class="subtitle">${_("Content")}</small>
|
||||
<span class="sr">> </span>${_("Textbooks")}
|
||||
</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"><span class="icon fa fa-plus" aria-hidden="true"></span> ${_("New Textbook")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main"></article>
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Why should I break my textbook into chapters?")}</h3>
|
||||
<p>${_("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.")}</p>
|
||||
</div>
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What if my book isn't divided into chapters?")}</h3>
|
||||
<p>${_("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.")}</p>
|
||||
</div>
|
||||
|
||||
<div class="bit external-help">
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about textbooks")}</a>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user