Commit 50128cfb by cahrens

Convert edit_subsection, edit_unit, and publishing to RESTful URLs.

STUD-844
parent 037cec6c
...@@ -7,6 +7,10 @@ the top. Include a label indicating the component affected. ...@@ -7,6 +7,10 @@ the top. Include a label indicating the component affected.
Blades: Enabled several Video Jasmine tests. BLD-463. Blades: Enabled several Video Jasmine tests. BLD-463.
Studio: Continued modification of Studio pages to follow a RESTful framework.
includes Settings pages, edit page for Subsection and Unit, and interfaces
for updating xblocks (xmodules) and getting their editing HTML.
Blades: Put 2nd "Hide output" button at top of test box & increase text size for Blades: Put 2nd "Hide output" button at top of test box & increase text size for
code response questions. BLD-126. code response questions. BLD-126.
......
...@@ -42,6 +42,7 @@ from xmodule.capa_module import CapaDescriptor ...@@ -42,6 +42,7 @@ from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.locator import BlockUsageLocator
from contentstore.views.component import ADVANCED_COMPONENT_TYPES from contentstore.views.component import ADVANCED_COMPONENT_TYPES
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
...@@ -133,10 +134,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -133,10 +134,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# just pick one vertical # just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0] descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
locator = loc_mapper().translate_location(course.location.course_id, descriptor.location, False, True)
resp = self.client.get_html(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) resp = self.client.get_html(locator.url_reverse('unit'))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# TODO: uncomment after edit_unit no longer using locations. # TODO: uncomment when video transcripts no longer require IDs.
# _test_no_locations(self, resp) # _test_no_locations(self, resp)
for expected in expected_types: for expected in expected_types:
...@@ -155,29 +156,22 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -155,29 +156,22 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_malformed_edit_unit_request(self): def test_malformed_edit_unit_request(self):
store = modulestore('direct') store = modulestore('direct')
import_from_xml(store, 'common/test/data/', ['simple']) _, course_items = import_from_xml(store, 'common/test/data/', ['simple'])
# just pick one vertical # just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0] descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
location = descriptor.location.replace(name='.' + descriptor.location.name) location = descriptor.location.replace(name='.' + descriptor.location.name)
locator = loc_mapper().translate_location(course_items[0].location.course_id, location, False, True)
resp = self.client.get_html(reverse('edit_unit', kwargs={'location': location.url()})) resp = self.client.get_html(locator.url_reverse('unit'))
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
_test_no_locations(self, resp, status_code=400) _test_no_locations(self, resp, status_code=400)
def check_edit_unit(self, test_course_name): def check_edit_unit(self, test_course_name):
import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name]) _, course_items = import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
items = modulestore().get_items(Location('i4x', 'edX', test_course_name, 'vertical', None, None)) items = modulestore().get_items(Location('i4x', 'edX', test_course_name, 'vertical', None, None))
# Assert is here to make sure that the course being tested actually has verticals. self._check_verticals(items, course_items[0].location.course_id)
self.assertGreater(len(items), 0)
for descriptor in items:
print "Checking ", descriptor.location.url()
print descriptor.__class__, descriptor.location
resp = self.client.get_html(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
# TODO: uncomment after edit_unit not using locations.
# _test_no_locations(self, resp)
def _lock_an_asset(self, content_store, course_location): def _lock_an_asset(self, content_store, course_location):
""" """
...@@ -1065,14 +1059,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -1065,14 +1059,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
target_location_namespace=course_location target_location_namespace=course_location
) )
# Unit test fails in Jenkins without this.
loc_mapper().translate_location(course_location.course_id, course_location, False, True)
items = module_store.get_items(stub_location.replace(category='vertical', name=None)) items = module_store.get_items(stub_location.replace(category='vertical', name=None))
self.assertGreater(len(items), 0) self._check_verticals(items, course_location.course_id)
for descriptor in items:
print "Checking {0}....".format(descriptor.location.url())
resp = self.client.get_html(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
# TODO: uncomment when edit_unit no longer has locations.
# _test_no_locations(self, resp)
# verify that we have the content in the draft store as well # verify that we have the content in the draft store as well
vertical = draft_store.get_item( vertical = draft_store.get_item(
...@@ -1355,6 +1346,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -1355,6 +1346,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
items = module_store.get_items(stub_location) items = module_store.get_items(stub_location)
self.assertEqual(len(items), 1) self.assertEqual(len(items), 1)
def _check_verticals(self, items, course_id):
""" Test getting the editing HTML for each vertical. """
# Assert is here to make sure that the course being tested actually has verticals (units) to check.
self.assertGreater(len(items), 0)
for descriptor in items:
unit_locator = loc_mapper().translate_location(course_id, descriptor.location, False, True)
print "Checking {0}....".format(unicode(unit_locator))
print descriptor.__class__, descriptor.location
resp = self.client.get_html(unit_locator.url_reverse('unit'))
self.assertEqual(resp.status_code, 200)
# TODO: uncomment when video transcripts no longer require IDs.
# _test_no_locations(self, resp)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE) @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class ContentStoreTest(ModuleStoreTestCase): class ContentStoreTest(ModuleStoreTestCase):
...@@ -1598,12 +1602,13 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1598,12 +1602,13 @@ class ContentStoreTest(ModuleStoreTestCase):
} }
resp = self.client.ajax_post('/xblock', section_data) resp = self.client.ajax_post('/xblock', section_data)
_test_no_locations(self, resp, html=False)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
data = parse_json(resp) data = parse_json(resp)
self.assertRegexpMatches( self.assertRegexpMatches(
data['id'], data['locator'],
r"^i4x://MITx/999/chapter/([0-9]|[a-f]){32}$" r"^MITx.999.Robot_Super_Course/branch/draft/block/chapter([0-9]|[a-f]){3}$"
) )
def test_capa_module(self): def test_capa_module(self):
...@@ -1619,7 +1624,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1619,7 +1624,7 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
payload = parse_json(resp) payload = parse_json(resp)
problem_loc = Location(payload['id']) problem_loc = loc_mapper().translate_locator_to_location(BlockUsageLocator(payload['locator']))
problem = get_modulestore(problem_loc).get_item(problem_loc) problem = get_modulestore(problem_loc).get_item(problem_loc)
# should be a CapaDescriptor # should be a CapaDescriptor
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor") self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
...@@ -1677,19 +1682,17 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1677,19 +1682,17 @@ class ContentStoreTest(ModuleStoreTestCase):
# go look at a subsection page # go look at a subsection page
subsection_location = loc.replace(category='sequential', name='test_sequence') subsection_location = loc.replace(category='sequential', name='test_sequence')
resp = self.client.get_html( subsection_locator = loc_mapper().translate_location(loc.course_id, subsection_location, False, True)
reverse('edit_subsection', kwargs={'location': subsection_location.url()}) resp = self.client.get_html(subsection_locator.url_reverse('subsection'))
)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# TODO: uncomment when grading and outline not using old locations. _test_no_locations(self, resp)
# _test_no_locations(self, resp)
# go look at the Edit page # go look at the Edit page
unit_location = loc.replace(category='vertical', name='test_vertical') unit_location = loc.replace(category='vertical', name='test_vertical')
resp = self.client.get_html( unit_locator = loc_mapper().translate_location(loc.course_id, unit_location, False, True)
reverse('edit_unit', kwargs={'location': unit_location.url()})) resp = self.client.get_html(unit_locator.url_reverse('unit'))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# TODO: uncomment when edit_unit not using old locations. # TODO: uncomment when video transcripts no longer require IDs.
# _test_no_locations(self, resp) # _test_no_locations(self, resp)
def delete_item(category, name): def delete_item(category, name):
...@@ -1899,8 +1902,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1899,8 +1902,7 @@ class ContentStoreTest(ModuleStoreTestCase):
""" """
new_location = loc_mapper().translate_location(location.course_id, location, False, True) new_location = loc_mapper().translate_location(location.course_id, location, False, True)
resp = self.client.get_html(new_location.url_reverse('course/', '')) resp = self.client.get_html(new_location.url_reverse('course/', ''))
# TODO: uncomment when i4x no longer in overview. _test_no_locations(self, resp)
# _test_no_locations(self, resp)
return resp return resp
...@@ -2033,7 +2035,8 @@ def _test_no_locations(test, resp, status_code=200, html=True): ...@@ -2033,7 +2035,8 @@ def _test_no_locations(test, resp, status_code=200, html=True):
# it checks that the HTML properly parses. However, it won't find i4x usages # it checks that the HTML properly parses. However, it won't find i4x usages
# in JavaScript blocks. # in JavaScript blocks.
content = resp.content content = resp.content
num_jump_to = len(re.findall(r"8000(\S)*jump_to/i4x", content)) num_jump_to_preview = len(re.findall(r"/preview/(\S)*jump_to/i4x", content))
total_i4x = len(re.findall(r"i4x", content)) num_jump_to_live = len(re.findall(r":8000/(\S)*jump_to/i4x", content))
hits = len(re.findall(r"i4x", content)) - num_jump_to_preview - num_jump_to_live
test.assertEqual(total_i4x - num_jump_to, 0, "i4x found outside of LMS jump-to links") test.assertEqual(hits, 0, "i4x found outside of LMS jump-to links")
...@@ -263,7 +263,7 @@ class ExportTestCase(CourseTestCase): ...@@ -263,7 +263,7 @@ class ExportTestCase(CourseTestCase):
parent_location=vertical.location, parent_location=vertical.location,
category='aawefawef' category='aawefawef'
) )
self._verify_export_failure('/edit/i4x://MITx/999/vertical/foo') self._verify_export_failure(u'/unit/MITx.999.Robot_Super_Course/branch/draft/block/foo')
def _verify_export_failure(self, expectedText): def _verify_export_failure(self, expectedText):
""" Export failure helper method. """ """ Export failure helper method. """
......
...@@ -9,6 +9,7 @@ from xmodule.capa_module import CapaDescriptor ...@@ -9,6 +9,7 @@ from xmodule.capa_module import CapaDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.modulestore.exceptions import ItemNotFoundError
class ItemTest(CourseTestCase): class ItemTest(CourseTestCase):
...@@ -30,7 +31,7 @@ class ItemTest(CourseTestCase): ...@@ -30,7 +31,7 @@ class ItemTest(CourseTestCase):
""" """
Get the item referenced by the locator from the modulestore Get the item referenced by the locator from the modulestore
""" """
store = modulestore('draft') if draft else modulestore() store = modulestore('draft') if draft else modulestore('direct')
return store.get_item(self.get_old_id(locator)) return store.get_item(self.get_old_id(locator))
def response_locator(self, response): def response_locator(self, response):
...@@ -251,3 +252,105 @@ class TestEditItem(ItemTest): ...@@ -251,3 +252,105 @@ class TestEditItem(ItemTest):
self.assertEqual(self.get_old_id(self.problem_locator).url(), children[0]) 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(unit1_locator).url(), children[2])
self.assertEqual(self.get_old_id(unit2_locator).url(), children[1]) 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))
...@@ -20,6 +20,7 @@ from xmodule.contentstore.django import contentstore, _CONTENTSTORE ...@@ -20,6 +20,7 @@ from xmodule.contentstore.django import contentstore, _CONTENTSTORE
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator
from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.tests.modulestore_config import TEST_MODULESTORE
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
...@@ -59,7 +60,7 @@ class Basetranscripts(CourseTestCase): ...@@ -59,7 +60,7 @@ class Basetranscripts(CourseTestCase):
'type': 'video' 'type': 'video'
} }
resp = self.client.ajax_post('/xblock', data) resp = self.client.ajax_post('/xblock', data)
self.item_location = json.loads(resp.content).get('id') self.item_location = self._get_location(resp)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# hI10vDNYz4M - valid Youtube ID with transcripts. # hI10vDNYz4M - valid Youtube ID with transcripts.
...@@ -72,6 +73,11 @@ class Basetranscripts(CourseTestCase): ...@@ -72,6 +73,11 @@ class Basetranscripts(CourseTestCase):
# Remove all transcripts for current module. # Remove all transcripts for current module.
self.clear_subs_content() self.clear_subs_content()
def _get_location(self, resp):
""" Returns the location (as a string) from the response returned by a create operation. """
locator = json.loads(resp.content).get('locator')
return loc_mapper().translate_locator_to_location(BlockUsageLocator(locator)).url()
def get_youtube_ids(self): def get_youtube_ids(self):
"""Return youtube speeds and ids.""" """Return youtube speeds and ids."""
item = modulestore().get_item(self.item_location) item = modulestore().get_item(self.item_location)
...@@ -205,7 +211,7 @@ class TestUploadtranscripts(Basetranscripts): ...@@ -205,7 +211,7 @@ class TestUploadtranscripts(Basetranscripts):
'type': 'non_video' 'type': 'non_video'
} }
resp = self.client.ajax_post('/xblock', data) resp = self.client.ajax_post('/xblock', data)
item_location = json.loads(resp.content).get('id') item_location = self._get_location(resp)
data = '<non_video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M" />' data = '<non_video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M" />'
modulestore().update_item(item_location, data) modulestore().update_item(item_location, data)
...@@ -416,7 +422,7 @@ class TestDownloadtranscripts(Basetranscripts): ...@@ -416,7 +422,7 @@ class TestDownloadtranscripts(Basetranscripts):
'type': 'videoalpha' 'type': 'videoalpha'
} }
resp = self.client.ajax_post('/xblock', data) resp = self.client.ajax_post('/xblock', data)
item_location = json.loads(resp.content).get('id') item_location = self._get_location(resp)
subs_id = str(uuid4()) subs_id = str(uuid4())
data = textwrap.dedent(""" data = textwrap.dedent("""
<videoalpha youtube="" sub="{}"> <videoalpha youtube="" sub="{}">
...@@ -666,7 +672,7 @@ class TestChecktranscripts(Basetranscripts): ...@@ -666,7 +672,7 @@ class TestChecktranscripts(Basetranscripts):
'type': 'not_video' 'type': 'not_video'
} }
resp = self.client.ajax_post('/xblock', data) resp = self.client.ajax_post('/xblock', data)
item_location = json.loads(resp.content).get('id') item_location = self._get_location(resp)
subs_id = str(uuid4()) subs_id = str(uuid4())
data = textwrap.dedent(""" data = textwrap.dedent("""
<not_video youtube="" sub="{}"> <not_video youtube="" sub="{}">
......
...@@ -14,7 +14,6 @@ from django.conf import settings ...@@ -14,7 +14,6 @@ from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.core.servers.basehttp import FileWrapper from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile from django.core.files.temp import NamedTemporaryFile
from django.core.exceptions import SuspiciousOperation, PermissionDenied from django.core.exceptions import SuspiciousOperation, PermissionDenied
...@@ -140,7 +139,7 @@ def import_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -140,7 +139,7 @@ def import_handler(request, tag=None, course_id=None, branch=None, version_guid=
"size": size, "size": size,
"deleteUrl": "", "deleteUrl": "",
"deleteType": "", "deleteType": "",
"url": location.url_reverse('import/', ''), "url": location.url_reverse('import'),
"thumbnailUrl": "" "thumbnailUrl": ""
}] }]
}) })
...@@ -252,8 +251,8 @@ def import_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -252,8 +251,8 @@ def import_handler(request, tag=None, course_id=None, branch=None, version_guid=
course_module = modulestore().get_item(old_location) course_module = modulestore().get_item(old_location)
return render_to_response('import.html', { return render_to_response('import.html', {
'context_course': course_module, 'context_course': course_module,
'successful_import_redirect_url': location.url_reverse("course/", ""), 'successful_import_redirect_url': location.url_reverse("course"),
'import_status_url': location.url_reverse("import_status/", "fillerName"), 'import_status_url': location.url_reverse("import_status", "fillerName"),
}) })
else: else:
return HttpResponseNotFound() return HttpResponseNotFound()
...@@ -313,7 +312,7 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -313,7 +312,7 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid=
# an _accept URL parameter will be preferred over HTTP_ACCEPT in the header. # an _accept URL parameter will be preferred over HTTP_ACCEPT in the header.
requested_format = request.REQUEST.get('_accept', request.META.get('HTTP_ACCEPT', 'text/html')) requested_format = request.REQUEST.get('_accept', request.META.get('HTTP_ACCEPT', 'text/html'))
export_url = location.url_reverse('export/', '') + '?_accept=application/x-tgz' export_url = location.url_reverse('export') + '?_accept=application/x-tgz'
if 'application/x-tgz' in requested_format: if 'application/x-tgz' in requested_format:
name = old_location.name name = old_location.name
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz") export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
...@@ -339,16 +338,16 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -339,16 +338,16 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid=
# if we have a nested exception, then we'll show the more generic error message # if we have a nested exception, then we'll show the more generic error message
pass pass
unit_locator = loc_mapper().translate_location(old_location.course_id, parent.location, False, True)
return render_to_response('export.html', { return render_to_response('export.html', {
'context_course': course_module, 'context_course': course_module,
'in_err': True, 'in_err': True,
'raw_err_msg': str(e), 'raw_err_msg': str(e),
'failed_module': failed_item, 'failed_module': failed_item,
'unit': unit, 'unit': unit,
'edit_unit_url': reverse('edit_unit', kwargs={ 'edit_unit_url': unit_locator.url_reverse("unit") if parent else "",
'location': parent.location 'course_home_url': location.url_reverse("course"),
}) if parent else '',
'course_home_url': location.url_reverse("course/", ""),
'export_url': export_url 'export_url': export_url
}) })
...@@ -359,7 +358,7 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -359,7 +358,7 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid=
'in_err': True, 'in_err': True,
'unit': None, 'unit': None,
'raw_err_msg': str(e), 'raw_err_msg': str(e),
'course_home_url': location.url_reverse("course/", ""), 'course_home_url': location.url_reverse("course"),
'export_url': export_url 'export_url': export_url
}) })
......
...@@ -31,7 +31,6 @@ from django.http import HttpResponseBadRequest ...@@ -31,7 +31,6 @@ from django.http import HttpResponseBadRequest
from xblock.fields import Scope from xblock.fields import Scope
from preview import handler_prefix, get_preview_html from preview import handler_prefix, get_preview_html
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from django.views.decorators.csrf import ensure_csrf_cookie
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
__all__ = ['orphan_handler', 'xblock_handler'] __all__ = ['orphan_handler', 'xblock_handler']
...@@ -57,7 +56,7 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -57,7 +56,7 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
all children and "all_versions" to delete from all (mongo) versions. all children and "all_versions" to delete from all (mongo) versions.
GET GET
json: returns representation of the xblock (locator id, data, and metadata). json: returns representation of the xblock (locator id, data, and metadata).
if ?fields=graderType, it returns the graderType for the unit instead of the above. if ?fields=graderType, it returns the graderType for the unit instead of the above.
html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view) html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view)
PUT or POST PUT or POST
json: if xblock locator is specified, update the xblock instance. The json payload can contain json: if xblock locator is specified, update the xblock instance. The json payload can contain
...@@ -68,6 +67,7 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -68,6 +67,7 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
to None! Absent ones will be left alone. to None! Absent ones will be left alone.
:nullout: which metadata fields to set to None :nullout: which metadata fields to set to None
:graderType: change how this unit is graded :graderType: change how this unit is graded
:publish: can be one of three values, 'make_public, 'make_private', or 'create_draft'
The JSON representation on the updated xblock (minus children) is returned. The JSON representation on the updated xblock (minus children) is returned.
if xblock locator is not specified, create a new xblock instance. The json playload can contain if xblock locator is not specified, create a new xblock instance. The json playload can contain
...@@ -118,13 +118,15 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -118,13 +118,15 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
return _delete_item_at_location(old_location, delete_children, delete_all_versions) return _delete_item_at_location(old_location, delete_children, delete_all_versions)
else: # Since we have a course_id, we are updating an existing xblock. else: # Since we have a course_id, we are updating an existing xblock.
return _save_item( return _save_item(
request,
locator, locator,
old_location, old_location,
data=request.json.get('data'), data=request.json.get('data'),
children=request.json.get('children'), children=request.json.get('children'),
metadata=request.json.get('metadata'), metadata=request.json.get('metadata'),
nullout=request.json.get('nullout'), nullout=request.json.get('nullout'),
grader_type=request.json.get('graderType') grader_type=request.json.get('graderType'),
publish=request.json.get('publish'),
) )
elif request.method in ('PUT', 'POST'): elif request.method in ('PUT', 'POST'):
return _create_item(request) return _create_item(request)
...@@ -135,11 +137,10 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -135,11 +137,10 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
) )
def _save_item(usage_loc, item_location, data=None, children=None, metadata=None, nullout=None, def _save_item(request, usage_loc, item_location, data=None, children=None, metadata=None, nullout=None,
grader_type=None grader_type=None, publish=None):
):
""" """
Saves xblock w/ its fields. Has special processing for grader_type and nullout and Nones in metadata. Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
to default). to default).
...@@ -161,6 +162,14 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None ...@@ -161,6 +162,14 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None
log.error("Can't find item by location.") log.error("Can't find item by location.")
return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404) return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404)
if publish:
if publish == 'make_private':
_xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location))
elif publish == 'create_draft':
# This clones the existing item location to a draft location (the draft is
# implicit, because modulestore is a Draft modulestore)
modulestore().convert_to_draft(item_location)
if data: if data:
store.update_item(item_location, data) store.update_item(item_location, data)
else: else:
...@@ -213,9 +222,18 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None ...@@ -213,9 +222,18 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None
'data': data, 'data': data,
'metadata': own_metadata(existing_item) 'metadata': own_metadata(existing_item)
} }
if grader_type is not None: if grader_type is not None:
result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type)) result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type))
# Make public after updating the xblock, in case the caller asked
# for both an update and a publish.
if publish and publish == 'make_public':
_xmodule_recurse(
existing_item,
lambda i: modulestore().publish(i.location, request.user.id)
)
# Note that children aren't being returned until we have a use case. # Note that children aren't being returned until we have a use case.
return JsonResponse(result) return JsonResponse(result)
...@@ -234,10 +252,7 @@ def _create_item(request): ...@@ -234,10 +252,7 @@ def _create_item(request):
raise PermissionDenied() raise PermissionDenied()
parent = get_modulestore(category).get_item(parent_location) parent = get_modulestore(category).get_item(parent_location)
# Necessary to set revision=None or else metadata inheritance does not work dest_location = parent_location.replace(category=category, name=uuid4().hex)
# (the ID with @draft will be used as the key in the inherited metadata map,
# and that is not expected by the code that later references it).
dest_location = parent_location.replace(category=category, name=uuid4().hex, revision=None)
# get the metadata, display_name, and definition from the request # get the metadata, display_name, and definition from the request
metadata = {} metadata = {}
...@@ -266,7 +281,7 @@ def _create_item(request): ...@@ -266,7 +281,7 @@ def _create_item(request):
course_location = loc_mapper().translate_locator_to_location(parent_locator, get_course=True) course_location = loc_mapper().translate_locator_to_location(parent_locator, get_course=True)
locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True) locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True)
return JsonResponse({'id': dest_location.url(), "locator": unicode(locator)}) return JsonResponse({"locator": unicode(locator)})
def _delete_item_at_location(item_location, delete_children=False, delete_all_versions=False): def _delete_item_at_location(item_location, delete_children=False, delete_all_versions=False):
......
...@@ -166,7 +166,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone", ...@@ -166,7 +166,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@wait(true) @wait(true)
$.ajax({ $.ajax({
type: 'DELETE', type: 'DELETE',
url: @model.urlRoot + "/" + @$el.data('locator') + "?" + $.param({recurse: true}) url: @model.url() + "?" + $.param({recurse: true})
}).success(=> }).success(=>
analytics.track "Deleted Draft", analytics.track "Deleted Draft",
...@@ -179,8 +179,8 @@ define ["jquery", "jquery.ui", "gettext", "backbone", ...@@ -179,8 +179,8 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
createDraft: (event) -> createDraft: (event) ->
@wait(true) @wait(true)
$.postJSON('/create_draft', { $.postJSON(@model.url(), {
id: @$el.data('id') publish: 'create_draft'
}, => }, =>
analytics.track "Created Draft", analytics.track "Created Draft",
course: course_location_analytics course: course_location_analytics
...@@ -193,8 +193,8 @@ define ["jquery", "jquery.ui", "gettext", "backbone", ...@@ -193,8 +193,8 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@wait(true) @wait(true)
@saveDraft() @saveDraft()
$.postJSON('/publish_draft', { $.postJSON(@model.url(), {
id: @$el.data('id') publish: 'make_public'
}, => }, =>
analytics.track "Published Draft", analytics.track "Published Draft",
course: course_location_analytics course: course_location_analytics
...@@ -205,16 +205,16 @@ define ["jquery", "jquery.ui", "gettext", "backbone", ...@@ -205,16 +205,16 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
setVisibility: (event) -> setVisibility: (event) ->
if @$('.visibility-select').val() == 'private' if @$('.visibility-select').val() == 'private'
target_url = '/unpublish_unit' action = 'make_private'
visibility = "private" visibility = "private"
else else
target_url = '/publish_draft' action = 'make_public'
visibility = "public" visibility = "public"
@wait(true) @wait(true)
$.postJSON(target_url, { $.postJSON(@model.url(), {
id: @$el.data('id') publish: action
}, => }, =>
analytics.track "Set Unit Visibility", analytics.track "Set Unit Visibility",
course: course_location_analytics course: course_location_analytics
......
...@@ -237,7 +237,7 @@ function createNewUnit(e) { ...@@ -237,7 +237,7 @@ function createNewUnit(e) {
function(data) { function(data) {
// redirect to the edit page // redirect to the edit page
window.location = "/edit/" + data['id']; window.location = "/unit/" + data['locator'];
}); });
} }
......
...@@ -207,7 +207,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -207,7 +207,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<div class="section-item"> <div class="section-item">
<div class="details"> <div class="details">
<a href="#" data-tooltip="${_('Expand/collapse this subsection')}" class="expand-collapse-icon expand"></a> <a href="#" data-tooltip="${_('Expand/collapse this subsection')}" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}"> <a href="${subsection_locator.url_reverse('subsection')}">
<span class="folder-icon"></span> <span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span> <span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a> </a>
......
...@@ -34,7 +34,7 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit" ...@@ -34,7 +34,7 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
</%block> </%block>
<%block name="content"> <%block name="content">
<div class="main-wrapper edit-state-${unit_state}" data-id="${unit_location}" data-locator="${unit_locator}"> <div class="main-wrapper edit-state-${unit_state}" data-locator="${unit_locator}">
<div class="inner-wrapper"> <div class="inner-wrapper">
<div class="alert editing-draft-alert"> <div class="alert editing-draft-alert">
<p class="alert-message"><strong>${_("You are editing a draft.")}</strong> <p class="alert-message"><strong>${_("You are editing a draft.")}</strong>
...@@ -135,6 +135,13 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit" ...@@ -135,6 +135,13 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
</article> </article>
</div> </div>
<%
ctx_loc = context_course.location
index_url = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True).url_reverse('course')
subsection_url = loc_mapper().translate_location(
ctx_loc.course_id, subsection.location, False, True
).url_reverse('subsection')
%>
<div class="sidebar"> <div class="sidebar">
<div class="unit-settings window"> <div class="unit-settings window">
<h4 class="header">${_("Unit Settings")}</h4> <h4 class="header">${_("Unit Settings")}</h4>
...@@ -157,7 +164,7 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit" ...@@ -157,7 +164,7 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
% endif % endif
${_("with the subsection {link_start}{name}{link_end}").format( ${_("with the subsection {link_start}{name}{link_end}").format(
name=subsection.display_name_with_default, name=subsection.display_name_with_default,
link_start='<a href="{url}">'.format(url=reverse('edit_subsection', kwargs={'location': subsection.location})), link_start='<a href="{url}">'.format(url=subsection_url),
link_end='</a>', link_end='</a>',
)} )}
</p> </p>
...@@ -180,14 +187,10 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit" ...@@ -180,14 +187,10 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
</div> </div>
<ol> <ol>
<li> <li>
<%
ctx_loc = context_course.location
index_url = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True).url_reverse('course/', '')
%>
<a href="${index_url}" class="section-item">${section.display_name_with_default}</a> <a href="${index_url}" class="section-item">${section.display_name_with_default}</a>
<ol> <ol>
<li> <li>
<a href="${reverse('edit_subsection', args=[subsection.location])}" class="section-item"> <a href="${subsection_url}" class="section-item">
<span class="folder-icon"></span> <span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span> <span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a> </a>
......
...@@ -31,7 +31,7 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -31,7 +31,7 @@ This def will enumerate through a passed in subsection and list all of the units
selected_class = '' selected_class = ''
%> %>
<div class="section-item ${selected_class}"> <div class="section-item ${selected_class}">
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item"> <a href="${unit_locator.url_reverse('unit')}" class="${unit_state}-item">
<span class="${unit.scope_ids.block_type}-icon"></span> <span class="${unit.scope_ids.block_type}-icon"></span>
<span class="unit-name">${unit.display_name_with_default}</span> <span class="unit-name">${unit.display_name_with_default}</span>
</a> </a>
......
...@@ -11,8 +11,6 @@ from ratelimitbackend import admin ...@@ -11,8 +11,6 @@ from ratelimitbackend import admin
admin.autodiscover() admin.autodiscover()
urlpatterns = patterns('', # nopep8 urlpatterns = patterns('', # nopep8
url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'),
url(r'^subsection/(?P<location>.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'),
url(r'^transcripts/upload$', 'contentstore.views.upload_transcripts', name='upload_transcripts'), url(r'^transcripts/upload$', 'contentstore.views.upload_transcripts', name='upload_transcripts'),
url(r'^transcripts/download$', 'contentstore.views.download_transcripts', name='download_transcripts'), url(r'^transcripts/download$', 'contentstore.views.download_transcripts', name='download_transcripts'),
...@@ -22,10 +20,6 @@ urlpatterns = patterns('', # nopep8 ...@@ -22,10 +20,6 @@ urlpatterns = patterns('', # nopep8
url(r'^transcripts/rename$', 'contentstore.views.rename_transcripts', name='rename_transcripts'), url(r'^transcripts/rename$', 'contentstore.views.rename_transcripts', name='rename_transcripts'),
url(r'^transcripts/save$', 'contentstore.views.save_transcripts', name='save_transcripts'), url(r'^transcripts/save$', 'contentstore.views.save_transcripts', name='save_transcripts'),
url(r'^create_draft$', 'contentstore.views.create_draft', name='create_draft'),
url(r'^publish_draft$', 'contentstore.views.publish_draft', name='publish_draft'),
url(r'^unpublish_unit$', 'contentstore.views.unpublish_unit', name='unpublish_unit'),
url(r'^preview/xblock/(?P<usage_id>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>[^/]*))?$', url(r'^preview/xblock/(?P<usage_id>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>[^/]*))?$',
'contentstore.views.preview_handler', name='preview_handler'), 'contentstore.views.preview_handler', name='preview_handler'),
...@@ -89,6 +83,8 @@ urlpatterns += patterns( ...@@ -89,6 +83,8 @@ urlpatterns += patterns(
'course_info_update_handler' 'course_info_update_handler'
), ),
url(r'(?ix)^course($|/){}$'.format(parsers.URL_RE_SOURCE), 'course_handler'), url(r'(?ix)^course($|/){}$'.format(parsers.URL_RE_SOURCE), 'course_handler'),
url(r'(?ix)^subsection($|/){}$'.format(parsers.URL_RE_SOURCE), 'subsection_handler'),
url(r'(?ix)^unit($|/){}$'.format(parsers.URL_RE_SOURCE), 'unit_handler'),
url(r'(?ix)^checklists/{}(/)?(?P<checklist_index>\d+)?$'.format(parsers.URL_RE_SOURCE), 'checklists_handler'), url(r'(?ix)^checklists/{}(/)?(?P<checklist_index>\d+)?$'.format(parsers.URL_RE_SOURCE), 'checklists_handler'),
url(r'(?ix)^orphan/{}$'.format(parsers.URL_RE_SOURCE), 'orphan_handler'), url(r'(?ix)^orphan/{}$'.format(parsers.URL_RE_SOURCE), 'orphan_handler'),
url(r'(?ix)^assets/{}(/)?(?P<asset_id>.+)?$'.format(parsers.URL_RE_SOURCE), 'assets_handler'), url(r'(?ix)^assets/{}(/)?(?P<asset_id>.+)?$'.format(parsers.URL_RE_SOURCE), 'assets_handler'),
......
...@@ -204,21 +204,16 @@ class LocMapperStore(object): ...@@ -204,21 +204,16 @@ class LocMapperStore(object):
self._decode_from_mongo(old_name), self._decode_from_mongo(old_name),
None) None)
elif usage_id == locator.usage_id: elif usage_id == locator.usage_id:
# figure out revision # Always return revision=None because the
# enforce the draft only if category in [..] logic # old draft module store wraps locations as draft before
if category in draft.DIRECT_ONLY_CATEGORIES: # trying to access things.
revision = None
elif locator.branch == candidate['draft_branch']:
revision = draft.DRAFT
else:
revision = None
return Location( return Location(
'i4x', 'i4x',
candidate['_id']['org'], candidate['_id']['org'],
candidate['_id']['course'], candidate['_id']['course'],
category, category,
self._decode_from_mongo(old_name), self._decode_from_mongo(old_name),
revision) None)
return None return None
def add_block_location_translator(self, location, old_course_id=None, usage_id=None): def add_block_location_translator(self, location, old_course_id=None, usage_id=None):
......
...@@ -778,11 +778,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -778,11 +778,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
children: A list of child item identifiers children: A list of child item identifiers
""" """
# We expect the children IDs to always be the non-draft version. With view refactoring self._update_single_item(location, {'definition.children': children})
# for split, we are now passing the draft version in some cases.
children_ids = [Location(child).replace(revision=None).url() for child in children]
self._update_single_item(location, {'definition.children': children_ids})
# recompute (and update) the metadata inheritance tree which is cached # recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(Location(location)) self.refresh_cached_metadata_inheritance_tree(Location(location))
# fire signal that we've written to DB # fire signal that we've written to DB
......
...@@ -274,7 +274,9 @@ class TestLocationMapper(unittest.TestCase): ...@@ -274,7 +274,9 @@ class TestLocationMapper(unittest.TestCase):
course_id=prob_locator.course_id, branch='draft', usage_id=prob_locator.usage_id course_id=prob_locator.course_id, branch='draft', usage_id=prob_locator.usage_id
) )
prob_location = loc_mapper().translate_locator_to_location(prob_locator) prob_location = loc_mapper().translate_locator_to_location(prob_locator)
self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', 'draft')) # Even though the problem was set as draft, we always return revision=None to work
# with old mongo/draft modulestores.
self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123', None))
prob_locator = BlockUsageLocator( prob_locator = BlockUsageLocator(
course_id=new_style_course_id, usage_id='problem2', branch='production' course_id=new_style_course_id, usage_id='problem2', branch='production'
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment