357 lines
15 KiB
Python
357 lines
15 KiB
Python
"""Tests for items views."""
|
|
|
|
import json
|
|
import datetime
|
|
from pytz import UTC
|
|
|
|
from contentstore.tests.utils import CourseTestCase
|
|
from xmodule.capa_module import CapaDescriptor
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule.modulestore.django import loc_mapper
|
|
from xmodule.modulestore.locator import BlockUsageLocator
|
|
from xmodule.modulestore.exceptions import ItemNotFoundError
|
|
|
|
|
|
class ItemTest(CourseTestCase):
|
|
""" Base test class for create, save, and delete """
|
|
def setUp(self):
|
|
super(ItemTest, self).setUp()
|
|
|
|
self.unicode_locator = unicode(loc_mapper().translate_location(
|
|
self.course.location.course_id, self.course.location, False, True
|
|
))
|
|
|
|
def get_old_id(self, locator):
|
|
"""
|
|
Converts new locator to old id format (forcing to non-draft).
|
|
"""
|
|
return loc_mapper().translate_locator_to_location(BlockUsageLocator(locator)).replace(revision=None)
|
|
|
|
def get_item_from_modulestore(self, locator, draft=False):
|
|
"""
|
|
Get the item referenced by the locator from the modulestore
|
|
"""
|
|
store = modulestore('draft') if draft else modulestore('direct')
|
|
return store.get_item(self.get_old_id(locator))
|
|
|
|
def response_locator(self, response):
|
|
"""
|
|
Get the locator (unicode representation) from the response payload
|
|
:param response:
|
|
"""
|
|
parsed = json.loads(response.content)
|
|
return parsed['locator']
|
|
|
|
def create_xblock(self, parent_locator=None, display_name=None, category=None, boilerplate=None):
|
|
data = {
|
|
'parent_locator': self.unicode_locator if parent_locator is None else parent_locator,
|
|
'category': category
|
|
}
|
|
if display_name is not None:
|
|
data['display_name'] = display_name
|
|
if boilerplate is not None:
|
|
data['boilerplate'] = boilerplate
|
|
return self.client.ajax_post('/xblock', json.dumps(data))
|
|
|
|
|
|
class DeleteItem(ItemTest):
|
|
"""Tests for '/xblock' DELETE url."""
|
|
def test_delete_static_page(self):
|
|
# Add static tab
|
|
resp = self.create_xblock(category='static_tab')
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
# Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore).
|
|
resp_content = json.loads(resp.content)
|
|
resp = self.client.delete('/xblock/' + resp_content['locator'])
|
|
self.assertEqual(resp.status_code, 204)
|
|
|
|
|
|
class TestCreateItem(ItemTest):
|
|
"""
|
|
Test the create_item handler thoroughly
|
|
"""
|
|
def test_create_nicely(self):
|
|
"""
|
|
Try the straightforward use cases
|
|
"""
|
|
# create a chapter
|
|
display_name = 'Nicely created'
|
|
resp = self.create_xblock(display_name=display_name, category='chapter')
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
# get the new item and check its category and display_name
|
|
chap_locator = self.response_locator(resp)
|
|
new_obj = self.get_item_from_modulestore(chap_locator)
|
|
self.assertEqual(new_obj.scope_ids.block_type, 'chapter')
|
|
self.assertEqual(new_obj.display_name, display_name)
|
|
self.assertEqual(new_obj.location.org, self.course.location.org)
|
|
self.assertEqual(new_obj.location.course, self.course.location.course)
|
|
|
|
# get the course and ensure it now points to this one
|
|
course = self.get_item_from_modulestore(self.unicode_locator)
|
|
self.assertIn(self.get_old_id(chap_locator).url(), course.children)
|
|
|
|
# use default display name
|
|
resp = self.create_xblock(parent_locator=chap_locator, category='vertical')
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
vert_locator = self.response_locator(resp)
|
|
|
|
# create problem w/ boilerplate
|
|
template_id = 'multiplechoice.yaml'
|
|
resp = self.create_xblock(
|
|
parent_locator=vert_locator,
|
|
category='problem',
|
|
boilerplate=template_id
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
prob_locator = self.response_locator(resp)
|
|
problem = self.get_item_from_modulestore(prob_locator, True)
|
|
# ensure it's draft
|
|
self.assertTrue(problem.is_draft)
|
|
# check against the template
|
|
template = CapaDescriptor.get_template(template_id)
|
|
self.assertEqual(problem.data, template['data'])
|
|
self.assertEqual(problem.display_name, template['metadata']['display_name'])
|
|
self.assertEqual(problem.markdown, template['metadata']['markdown'])
|
|
|
|
def test_create_item_negative(self):
|
|
"""
|
|
Negative tests for create_item
|
|
"""
|
|
# non-existent boilerplate: creates a default
|
|
resp = self.create_xblock(category='problem', boilerplate='nosuchboilerplate.yaml')
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
|
|
class TestEditItem(ItemTest):
|
|
"""
|
|
Test xblock update.
|
|
"""
|
|
def setUp(self):
|
|
""" Creates the test course structure and a couple problems to 'edit'. """
|
|
super(TestEditItem, self).setUp()
|
|
# create a chapter
|
|
display_name = 'chapter created'
|
|
resp = self.create_xblock(display_name=display_name, category='chapter')
|
|
chap_locator = self.response_locator(resp)
|
|
resp = self.create_xblock(parent_locator=chap_locator, category='sequential')
|
|
self.seq_locator = self.response_locator(resp)
|
|
self.seq_update_url = '/xblock/' + self.seq_locator
|
|
|
|
# create problem w/ boilerplate
|
|
template_id = 'multiplechoice.yaml'
|
|
resp = self.create_xblock(parent_locator=self.seq_locator, category='problem', boilerplate=template_id)
|
|
self.problem_locator = self.response_locator(resp)
|
|
self.problem_update_url = '/xblock/' + self.problem_locator
|
|
|
|
self.course_update_url = '/xblock/' + self.unicode_locator
|
|
|
|
def test_delete_field(self):
|
|
"""
|
|
Sending null in for a field 'deletes' it
|
|
"""
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'metadata': {'rerandomize': 'onreset'}}
|
|
)
|
|
problem = self.get_item_from_modulestore(self.problem_locator, True)
|
|
self.assertEqual(problem.rerandomize, 'onreset')
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'metadata': {'rerandomize': None}}
|
|
)
|
|
problem = self.get_item_from_modulestore(self.problem_locator, True)
|
|
self.assertEqual(problem.rerandomize, 'never')
|
|
|
|
def test_null_field(self):
|
|
"""
|
|
Sending null in for a field 'deletes' it
|
|
"""
|
|
problem = self.get_item_from_modulestore(self.problem_locator, True)
|
|
self.assertIsNotNone(problem.markdown)
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'nullout': ['markdown']}
|
|
)
|
|
problem = self.get_item_from_modulestore(self.problem_locator, True)
|
|
self.assertIsNone(problem.markdown)
|
|
|
|
def test_date_fields(self):
|
|
"""
|
|
Test setting due & start dates on sequential
|
|
"""
|
|
sequential = self.get_item_from_modulestore(self.seq_locator)
|
|
self.assertIsNone(sequential.due)
|
|
self.client.ajax_post(
|
|
self.seq_update_url,
|
|
data={'metadata': {'due': '2010-11-22T04:00Z'}}
|
|
)
|
|
sequential = self.get_item_from_modulestore(self.seq_locator)
|
|
self.assertEqual(sequential.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
|
|
self.client.ajax_post(
|
|
self.seq_update_url,
|
|
data={'metadata': {'start': '2010-09-12T14:00Z'}}
|
|
)
|
|
sequential = self.get_item_from_modulestore(self.seq_locator)
|
|
self.assertEqual(sequential.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
|
|
self.assertEqual(sequential.start, datetime.datetime(2010, 9, 12, 14, 0, tzinfo=UTC))
|
|
|
|
def test_delete_child(self):
|
|
"""
|
|
Test deleting a child.
|
|
"""
|
|
# Create 2 children of main course.
|
|
resp_1 = self.create_xblock(display_name='child 1', category='chapter')
|
|
resp_2 = self.create_xblock(display_name='child 2', category='chapter')
|
|
chapter1_locator = self.response_locator(resp_1)
|
|
chapter2_locator = self.response_locator(resp_2)
|
|
|
|
course = self.get_item_from_modulestore(self.unicode_locator)
|
|
self.assertIn(self.get_old_id(chapter1_locator).url(), course.children)
|
|
self.assertIn(self.get_old_id(chapter2_locator).url(), course.children)
|
|
|
|
# Remove one child from the course.
|
|
resp = self.client.ajax_post(
|
|
self.course_update_url,
|
|
data={'children': [chapter2_locator]}
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
# Verify that the child is removed.
|
|
course = self.get_item_from_modulestore(self.unicode_locator)
|
|
self.assertNotIn(self.get_old_id(chapter1_locator).url(), course.children)
|
|
self.assertIn(self.get_old_id(chapter2_locator).url(), course.children)
|
|
|
|
def test_reorder_children(self):
|
|
"""
|
|
Test reordering children that can be in the draft store.
|
|
"""
|
|
# Create 2 child units and re-order them. There was a bug about @draft getting added
|
|
# to the IDs.
|
|
unit_1_resp = self.create_xblock(parent_locator=self.seq_locator, category='vertical')
|
|
unit_2_resp = self.create_xblock(parent_locator=self.seq_locator, category='vertical')
|
|
unit1_locator = self.response_locator(unit_1_resp)
|
|
unit2_locator = self.response_locator(unit_2_resp)
|
|
|
|
# The sequential already has a child defined in the setUp (a problem).
|
|
# Children must be on the sequential to reproduce the original bug,
|
|
# as it is important that the parent (sequential) NOT be in the draft store.
|
|
children = self.get_item_from_modulestore(self.seq_locator).children
|
|
self.assertEqual(self.get_old_id(unit1_locator).url(), children[1])
|
|
self.assertEqual(self.get_old_id(unit2_locator).url(), children[2])
|
|
|
|
resp = self.client.ajax_post(
|
|
self.seq_update_url,
|
|
data={'children': [self.problem_locator, unit2_locator, unit1_locator]}
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
children = self.get_item_from_modulestore(self.seq_locator).children
|
|
self.assertEqual(self.get_old_id(self.problem_locator).url(), children[0])
|
|
self.assertEqual(self.get_old_id(unit1_locator).url(), children[2])
|
|
self.assertEqual(self.get_old_id(unit2_locator).url(), children[1])
|
|
|
|
def test_make_public(self):
|
|
""" Test making a private problem public (publishing it). """
|
|
# When the problem is first created, it is only in draft (because of its category).
|
|
with self.assertRaises(ItemNotFoundError):
|
|
self.get_item_from_modulestore(self.problem_locator, False)
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'publish': 'make_public'}
|
|
)
|
|
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
|
|
|
def test_make_private(self):
|
|
""" Test making a public problem private (un-publishing it). """
|
|
# Make problem public.
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'publish': 'make_public'}
|
|
)
|
|
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
|
# Now make it private
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'publish': 'make_private'}
|
|
)
|
|
with self.assertRaises(ItemNotFoundError):
|
|
self.get_item_from_modulestore(self.problem_locator, False)
|
|
|
|
def test_make_draft(self):
|
|
""" Test creating a draft version of a public problem. """
|
|
# Make problem public.
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'publish': 'make_public'}
|
|
)
|
|
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
|
# Now make it draft, which means both versions will exist.
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'publish': 'create_draft'}
|
|
)
|
|
# Update the draft version and check that published is different.
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'metadata': {'due': '2077-10-10T04:00Z'}}
|
|
)
|
|
published = self.get_item_from_modulestore(self.problem_locator, False)
|
|
self.assertIsNone(published.due)
|
|
draft = self.get_item_from_modulestore(self.problem_locator, True)
|
|
self.assertEqual(draft.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
|
|
|
|
def test_make_public_with_update(self):
|
|
""" Update a problem and make it public at the same time. """
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={
|
|
'metadata': {'due': '2077-10-10T04:00Z'},
|
|
'publish': 'make_public'
|
|
}
|
|
)
|
|
published = self.get_item_from_modulestore(self.problem_locator, False)
|
|
self.assertEqual(published.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
|
|
|
|
def test_make_private_with_update(self):
|
|
""" Make a problem private and update it at the same time. """
|
|
# Make problem public.
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'publish': 'make_public'}
|
|
)
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={
|
|
'metadata': {'due': '2077-10-10T04:00Z'},
|
|
'publish': 'make_private'
|
|
}
|
|
)
|
|
with self.assertRaises(ItemNotFoundError):
|
|
self.get_item_from_modulestore(self.problem_locator, False)
|
|
draft = self.get_item_from_modulestore(self.problem_locator, True)
|
|
self.assertEqual(draft.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
|
|
|
|
def test_create_draft_with_update(self):
|
|
""" Create a draft and update it at the same time. """
|
|
# Make problem public.
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={'publish': 'make_public'}
|
|
)
|
|
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
|
# Now make it draft, which means both versions will exist.
|
|
self.client.ajax_post(
|
|
self.problem_update_url,
|
|
data={
|
|
'metadata': {'due': '2077-10-10T04:00Z'},
|
|
'publish': 'create_draft'
|
|
}
|
|
)
|
|
published = self.get_item_from_modulestore(self.problem_locator, False)
|
|
self.assertIsNone(published.due)
|
|
draft = self.get_item_from_modulestore(self.problem_locator, True)
|
|
self.assertEqual(draft.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
|