Files
edx-platform/cms/djangoapps/contentstore/views/tests/test_textbooks.py
Kyle McCormick 8e9f94424d feat!: Flip Studio MFE Waffle Flags to be On-By-Default (#36495)
This makes nearly all of Studio React-by-default by replacing the
"opt-in-to-React" flags with a set of parallel
"opt-out-of-React-and-use-the-legacy-experience" flags. Here is the
mapping:

* `contentstore.new_studio_mfe.use_new_unit_page` ->
  `!legacy_studio.unit_editor`
* `new_core_editors.use_new_problem_editor` ->
  `!legacy_studio.problem_editor`
* `new_core_editors.use_new_text_editor` ->
  `!legacy_studio.text_editor`
* `new_core_editors.use_new_video_editor` ->
  `!legacy_studio.video_editor`
* `new_studio_mfe.use_new_home_page` ->
  `!legacy_studio.home`
* `contentstore.new_studio_mfe.use_new_custom_pages` ->
  `!legacy_studio.custom_pages`
* `contentstore.new_studio_mfe.use_new_schedule_details_page` ->
  `!legacy_studio.schedule_details`
* `contentstore.new_studio_mfe.use_new_advanced_settings_page` ->
  `!legacy_studio.advanced_settings`
* `contentstore.new_studio_mfe.use_new_grading_page` ->
  `!legacy_studio.grading`
* `contentstore.new_studio_mfe.use_new_updates_page` ->
  `!legacy_studio.updates`
* `contentstore.new_studio_mfe.use_new_import_page` ->
  `!legacy_studio.import`
* `contentstore.new_studio_mfe.use_new_export_page` ->
  `!legacy_studio.export`
* `contentstore.new_studio_mfe.use_new_files_uploads_page` ->
  `!legacy_studio.files_uploads`
* `contentstore.new_studio_mfe.use_new_course_outline_page` ->
  `!legacy_studio.course_outline`
* `contentstore.new_studio_mfe.use_new_course_team_page` ->
  `!legacy_studio.course_team`
* `contentstore.new_studio_mfe.use_new_certificates_page` ->
  `!legacy_studio.certificates`
* `contentstore.new_studio_mfe.use_new_textbooks_page` ->
  `!legacy_studio.textbooks`
* `contentstore.new_studio_mfe.use_new_group_configurations_page` ->
  `!legacy_studio.configurations`

Part of: https://github.com/openedx/edx-platform/issues/36275
2025-04-24 12:34:35 -04:00

392 lines
14 KiB
Python

""" Test cases for the textbook index page. """
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
from ..course import TextbookValidationError, validate_textbook_json, validate_textbooks_json
class TextbookIndexTestCase(CourseTestCase):
"Test cases for the textbook index page"
def setUp(self):
"Set the URL for tests"
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)
def test_view_index_xhr(self):
"Check that we get a JSON response when requested via AJAX"
resp = self.client.get(
self.url,
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content.decode('utf-8'))
self.assertEqual(self.course.pdf_textbooks, obj)
def test_view_index_xhr_content(self):
"Check that the response maps to the content of the modulestore"
content = [
{
"tab_title": "my textbook",
"url": "/abc.pdf",
"id": "992"
}, {
"tab_title": "pineapple",
"id": "0pineapple",
"chapters": [
{
"title": "The Fruit",
"url": "/a/b/fruit.pdf",
}, {
"title": "The Legend",
"url": "/b/c/legend.pdf",
}
]
}
]
self.course.pdf_textbooks = content
self.save_course()
resp = self.client.get(
self.url,
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content.decode('utf-8'))
self.assertEqual(content, obj)
def test_view_index_xhr_put(self):
"Check that you can save information to the server"
textbooks = [
{"tab_title": "Hi, mom!"},
{"tab_title": "Textbook 2"},
]
resp = self.client.put(
self.url,
data=json.dumps(textbooks),
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assertEqual(resp.status_code, 200)
# should be the same, except for added ID
no_ids = []
self.reload_course()
for textbook in self.course.pdf_textbooks:
del textbook["id"]
no_ids.append(textbook)
self.assertEqual(no_ids, textbooks)
def test_view_index_xhr_put_invalid(self):
"Check that you can't save invalid JSON"
resp = self.client.put(
self.url,
data="invalid",
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assertEqual(resp.status_code, 400)
obj = json.loads(resp.content.decode('utf-8'))
self.assertIn("error", obj)
class TextbookCreateTestCase(CourseTestCase):
"Test cases for creating a new PDF textbook"
def setUp(self):
"Set up a url and some textbook content for tests"
super().setUp()
self.url = reverse_course_url('textbooks_list_handler', self.course.id)
self.textbook = {
"tab_title": "Economics",
"chapters": {
"title": "Chapter 1",
"url": "/a/b/c/ch1.pdf",
}
}
def test_happy_path(self):
"Test that you can create a textbook"
resp = self.client.post(
self.url,
data=json.dumps(self.textbook),
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(resp.status_code, 201)
self.assertIn("Location", resp)
textbook = json.loads(resp.content.decode('utf-8'))
self.assertIn("id", textbook)
del textbook["id"]
self.assertEqual(self.textbook, textbook)
def test_valid_id(self):
"Textbook IDs must begin with a number; try a valid one"
self.textbook["id"] = "7x5"
resp = self.client.post(
self.url,
data=json.dumps(self.textbook),
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(resp.status_code, 201)
textbook = json.loads(resp.content.decode('utf-8'))
self.assertEqual(self.textbook, textbook)
def test_invalid_id(self):
"Textbook IDs must begin with a number; try an invalid one"
self.textbook["id"] = "xxx"
resp = self.client.post(
self.url,
data=json.dumps(self.textbook),
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(resp.status_code, 400)
self.assertNotIn("Location", resp)
class TextbookDetailTestCase(CourseTestCase):
"Test cases for the `textbook_detail_handler` view"
def setUp(self):
"Set some useful content and URLs for tests"
super().setUp()
self.textbook1 = {
"tab_title": "Economics",
"id": 1,
"chapters": {
"title": "Chapter 1",
"url": "/a/b/c/ch1.pdf",
}
}
self.url1 = self.get_details_url("1")
self.textbook2 = {
"tab_title": "Algebra",
"id": 2,
"chapters": {
"title": "Chapter 11",
"url": "/a/b/ch11.pdf",
}
}
self.url2 = self.get_details_url("2")
self.course.pdf_textbooks = [self.textbook1, self.textbook2]
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
self.save_course()
self.url_nonexist = self.get_details_url("1=20")
def get_details_url(self, textbook_id):
"""
Returns the URL for textbook detail handler.
"""
return reverse_course_url(
'textbooks_detail_handler',
self.course.id,
kwargs={'textbook_id': textbook_id}
)
def test_get_1(self):
"Get the first textbook"
resp = self.client.get(self.url1)
self.assertEqual(resp.status_code, 200)
compare = json.loads(resp.content.decode('utf-8'))
self.assertEqual(compare, self.textbook1)
def test_get_2(self):
"Get the second textbook"
resp = self.client.get(self.url2)
self.assertEqual(resp.status_code, 200)
compare = json.loads(resp.content.decode('utf-8'))
self.assertEqual(compare, self.textbook2)
def test_get_nonexistant(self):
"Get a nonexistent textbook"
resp = self.client.get(self.url_nonexist)
self.assertEqual(resp.status_code, 404)
def test_delete(self):
"Delete a textbook by ID"
resp = self.client.delete(self.url1)
self.assertEqual(resp.status_code, 204)
self.reload_course()
self.assertEqual(self.course.pdf_textbooks, [self.textbook2])
def test_delete_nonexistant(self):
"Delete a textbook by ID, when the ID doesn't match an existing textbook"
resp = self.client.delete(self.url_nonexist)
self.assertEqual(resp.status_code, 404)
self.reload_course()
self.assertEqual(self.course.pdf_textbooks, [self.textbook1, self.textbook2])
def test_create_new_by_id(self):
"Create a textbook by ID"
textbook = {
"tab_title": "a new textbook",
"url": "supercool.pdf",
"id": "1supercool",
}
url = self.get_details_url("1supercool")
resp = self.client.post(
url,
data=json.dumps(textbook),
content_type="application/json",
)
self.assertEqual(resp.status_code, 201)
resp2 = self.client.get(url)
self.assertEqual(resp2.status_code, 200)
compare = json.loads(resp2.content.decode('utf-8'))
self.assertEqual(compare, textbook)
self.reload_course()
self.assertEqual(
self.course.pdf_textbooks,
[self.textbook1, self.textbook2, textbook]
)
def test_replace_by_id(self):
"Create a textbook by ID, overwriting an existing textbook ID"
replacement = {
"tab_title": "You've been replaced!",
"url": "supercool.pdf",
"id": "2",
}
resp = self.client.post(
self.url2,
data=json.dumps(replacement),
content_type="application/json",
)
self.assertEqual(resp.status_code, 201)
resp2 = self.client.get(self.url2)
self.assertEqual(resp2.status_code, 200)
compare = json.loads(resp2.content.decode('utf-8'))
self.assertEqual(compare, replacement)
course = self.store.get_item(self.course.location)
self.assertEqual(
course.pdf_textbooks,
[self.textbook1, replacement]
)
class TextbookValidationTestCase(TestCase):
"Tests for the code to validate the structure of a PDF textbook"
def setUp(self):
"Set some useful content for tests"
super().setUp()
self.tb1 = {
"tab_title": "Hi, mom!",
"url": "/mom.pdf"
}
self.tb2 = {
"tab_title": "Hi, dad!",
"chapters": [
{
"title": "Baseball",
"url": "baseball.pdf",
}, {
"title": "Basketball",
"url": "crazypants.pdf",
}
]
}
self.textbooks = [self.tb1, self.tb2]
def test_happy_path_plural(self):
"Test that the plural validator works properly"
result = validate_textbooks_json(json.dumps(self.textbooks))
self.assertEqual(self.textbooks, result)
def test_happy_path_singular_1(self):
"Test that the singular validator works properly"
result = validate_textbook_json(json.dumps(self.tb1))
self.assertEqual(self.tb1, result)
def test_happy_path_singular_2(self):
"Test that the singular validator works properly, with different data"
result = validate_textbook_json(json.dumps(self.tb2))
self.assertEqual(self.tb2, result)
def test_valid_id(self):
"Test that a valid ID doesn't trip the validator, and comes out unchanged"
self.tb1["id"] = 1
result = validate_textbook_json(json.dumps(self.tb1))
self.assertEqual(self.tb1, result)
def test_invalid_id(self):
"Test that an invalid ID trips the validator"
self.tb1["id"] = "abc"
with self.assertRaises(TextbookValidationError):
validate_textbook_json(json.dumps(self.tb1))
def test_invalid_json_plural(self):
"Test that invalid JSON trips the plural validator"
with self.assertRaises(TextbookValidationError):
validate_textbooks_json("[{'abc'}]")
def test_invalid_json_singular(self):
"Test that invalid JSON trips the singluar validator"
with self.assertRaises(TextbookValidationError):
validate_textbook_json("[{1]}")
def test_wrong_json_plural(self):
"Test that a JSON object trips the plural validators (requires a list)"
with self.assertRaises(TextbookValidationError):
validate_textbooks_json('{"tab_title": "Hi, mom!"}')
def test_wrong_json_singular(self):
"Test that a JSON list trips the plural validators (requires an object)"
with self.assertRaises(TextbookValidationError):
validate_textbook_json('[{"tab_title": "Hi, mom!"}, {"tab_title": "Hi, dad!"}]')
def test_no_tab_title_plural(self):
"Test that `tab_title` is required for the plural validator"
with self.assertRaises(TextbookValidationError):
validate_textbooks_json('[{"url": "/textbook.pdf"}]')
def test_no_tab_title_singular(self):
"Test that `tab_title` is required for the singular validator"
with self.assertRaises(TextbookValidationError):
validate_textbook_json('{"url": "/textbook.pdf"}')
def test_duplicate_ids(self):
"Test that duplicate IDs in the plural validator trips the validator"
textbooks = [{
"tab_title": "name one",
"url": "one.pdf",
"id": 1,
}, {
"tab_title": "name two",
"url": "two.pdf",
"id": 1,
}]
with self.assertRaises(TextbookValidationError):
validate_textbooks_json(json.dumps(textbooks))