Commit a170c6f4 by cahrens

Change save_item and create_item to RESTful URL.

Part of STUD-847.
parent dffbb8a3
......@@ -385,3 +385,18 @@ def create_other_user(_step, name, has_extra_perms, role_name):
@step('I log out')
def log_out(_step):
world.visit('logout')
@step(u'I click on "edit a draft"$')
def i_edit_a_draft(_step):
world.css_click("a.create-draft")
@step(u'I click on "replace with draft"$')
def i_edit_a_draft(_step):
world.css_click("a.publish-draft")
@step(u'I publish the unit$')
def publish_unit(_step):
world.select_option('visibility-select', 'public')
......@@ -87,13 +87,18 @@ def add_component_category(step, component, category):
@step(u'I delete all components$')
def delete_all_components(step):
count = len(world.css_find('ol.components li.component'))
step.given('I delete "' + str(count) + '" component')
@step(u'I delete "([^"]*)" component$')
def delete_components(step, number):
world.wait_for_xmodule()
delete_btn_css = 'a.delete-button'
prompt_css = 'div#prompt-warning'
btn_css = '{} a.button.action-primary'.format(prompt_css)
saving_mini_css = 'div#page-notification .wrapper-notification-mini'
count = len(world.css_find('ol.components li.component'))
for _ in range(int(count)):
for _ in range(int(number)):
world.css_click(delete_btn_css)
assert_true(
world.is_css_present('{}.is-shown'.format(prompt_css)),
......
......@@ -81,6 +81,21 @@ Feature: CMS.Problem Editor
When I edit and select Settings
Then Edit High Level Source is visible
# This is a very specific scenario that was failing with some of the
# DB rearchitecture changes. It had to do with children IDs being stored
# with @draft at the end. To reproduce, must update children while in draft mode.
Scenario: Problems can be deleted after being public
Given I have created a Blank Common Problem
And I have created another Blank Common Problem
When I publish the unit
And I click on "edit a draft"
And I delete "1" component
And I click on "replace with draft"
And I click on "edit a draft"
And I delete "1" component
Then I see no components
# Disabled 11/13/2013 after failing in master
# The screenshot showed that the LaTeX editor had the text "hi",
# but Selenium timed out waiting for the text to appear.
......
......@@ -19,6 +19,11 @@ SHOW_ANSWER = "Show Answer"
@step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step):
world.create_course_with_unit()
step.given("I have created another Blank Common Problem")
@step('I have created another Blank Common Problem$')
def i_create_new_common_problem(step):
world.create_component_instance(
step=step,
category='problem',
......@@ -218,11 +223,6 @@ def i_import_the_file(_step, filename):
import_file(filename)
@step(u'I click on "edit a draft"$')
def i_edit_a_draft(_step):
world.css_click("a.create-draft")
@step(u'I go to the vertical "([^"]*)"$')
def i_go_to_vertical(_step, vertical):
world.css_click("span:contains('{0}')".format(vertical))
......
......@@ -398,9 +398,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(course.tabs, expected_tabs)
def test_static_tab_reordering(self):
def get_tab_locator(tab):
tab_location = 'i4x://MITx/999/static_tab/{0}'.format(tab['url_slug'])
return unicode(loc_mapper().translate_location(
course.location.course_id, Location(tab_location), False, True
))
module_store = modulestore('direct')
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
locator = _course_factory_create_course()
course_location = loc_mapper().translate_locator_to_location(locator)
ItemFactory.create(
parent_location=course_location,
......@@ -411,23 +417,23 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
category="static_tab",
display_name="Static_2")
course = module_store.get_item(Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None]))
course = module_store.get_item(course_location)
# reverse the ordering
reverse_tabs = []
for tab in course.tabs:
if tab['type'] == 'static_tab':
reverse_tabs.insert(0, 'i4x://edX/999/static_tab/{0}'.format(tab['url_slug']))
reverse_tabs.insert(0, get_tab_locator(tab))
self.client.ajax_post(reverse('reorder_static_tabs'), {'tabs': reverse_tabs})
course = module_store.get_item(Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None]))
course = module_store.get_item(course_location)
# compare to make sure that the tabs information is in the expected order after the server call
course_tabs = []
for tab in course.tabs:
if tab['type'] == 'static_tab':
course_tabs.append('i4x://edX/999/static_tab/{0}'.format(tab['url_slug']))
course_tabs.append(get_tab_locator(tab))
self.assertEqual(reverse_tabs, course_tabs)
......@@ -1528,22 +1534,22 @@ class ContentStoreTest(ModuleStoreTestCase):
resp = self._show_course_overview(loc)
self.assertContains(
resp,
'<article class="courseware-overview" data-id="i4x://MITx/999/course/Robot_Super_Course">',
'<article class="courseware-overview" data-locator="MITx.999.Robot_Super_Course/branch/draft/block/Robot_Super_Course">',
status_code=200,
html=True
)
def test_create_item(self):
"""Test cloning an item. E.g. creating a new section"""
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
"""Test creating a new xblock instance."""
locator = _course_factory_create_course()
section_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'parent_locator': unicode(locator),
'category': 'chapter',
'display_name': 'Section One',
}
resp = self.client.ajax_post(reverse('create_item'), section_data)
resp = self.client.ajax_post('/xblock', section_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
......@@ -1554,14 +1560,14 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_capa_module(self):
"""Test that a problem treats markdown specially."""
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
locator = _course_factory_create_course()
problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'parent_locator': unicode(locator),
'category': 'problem'
}
resp = self.client.ajax_post(reverse('create_item'), problem_data)
resp = self.client.ajax_post('/xblock', problem_data)
self.assertEqual(resp.status_code, 200)
payload = parse_json(resp)
......@@ -1911,7 +1917,7 @@ class MetadataSaveTestCase(ModuleStoreTestCase):
def _create_course(test, course_data):
"""
Creates a course and verifies the URL returned in the response..
Creates a course via an AJAX request and verifies the URL returned in the response.
"""
course_id = _get_course_id(course_data)
new_location = loc_mapper().translate_location(course_id, CourseDescriptor.id_to_location(course_id), False, True)
......@@ -1923,6 +1929,14 @@ def _create_course(test, course_data):
test.assertEqual(data['url'], new_location.url_reverse("course/", ""))
def _course_factory_create_course():
"""
Creates a course via the CourseFactory and returns the locator for it.
"""
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
return loc_mapper().translate_location(course.location.course_id, course.location, False, True)
def _get_course_id(test_course_data):
"""Returns the course ID (org/number/run)."""
return "{org}/{number}/{run}".format(**test_course_data)
......@@ -3,109 +3,110 @@
import json
import datetime
from pytz import UTC
from django.core.urlresolvers import reverse
from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
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
class DeleteItem(CourseTestCase):
"""Tests for '/xblock' DELETE url."""
class ItemTest(CourseTestCase):
""" Base test class for create, save, and delete """
def setUp(self):
""" Creates the test course with a static page in it. """
super(DeleteItem, self).setUp()
self.course = CourseFactory.create(org='mitX', number='333', display_name='Dummy Course')
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.
"""
return loc_mapper().translate_locator_to_location(BlockUsageLocator(locator))
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()
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
data = json.dumps({
'parent_location': 'i4x://mitX/333/course/Dummy_Course',
'category': 'static_tab'
})
resp = self.client.post(
reverse('create_item'),
data,
content_type="application/json"
)
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(resp_content['update_url'])
resp = self.client.delete('/xblock/' + resp_content['locator'])
self.assertEqual(resp.status_code, 204)
class TestCreateItem(CourseTestCase):
class TestCreateItem(ItemTest):
"""
Test the create_item handler thoroughly
"""
def response_id(self, response):
"""
Get the id from the response payload
:param response:
"""
parsed = json.loads(response.content)
return parsed['id']
def test_create_nicely(self):
"""
Try the straightforward use cases
"""
# create a chapter
display_name = 'Nicely created'
resp = self.client.post(
reverse('create_item'),
json.dumps({
'parent_location': self.course.location.url(),
'display_name': display_name,
'category': 'chapter'
}),
content_type="application/json"
)
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_location = self.response_id(resp)
new_obj = modulestore().get_item(chap_location)
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 = modulestore().get_item(self.course.location)
self.assertIn(chap_location, course.children)
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.client.post(
reverse('create_item'),
json.dumps({
'parent_location': chap_location,
'category': 'vertical'
}),
content_type="application/json"
)
resp = self.create_xblock(parent_locator=chap_locator, category='vertical')
self.assertEqual(resp.status_code, 200)
vert_location = self.response_id(resp)
vert_locator = self.response_locator(resp)
# create problem w/ boilerplate
template_id = 'multiplechoice.yaml'
resp = self.client.post(
reverse('create_item'),
json.dumps({
'parent_location': vert_location,
'category': 'problem',
'boilerplate': template_id
}),
content_type="application/json"
resp = self.create_xblock(
parent_locator=vert_locator,
category='problem',
boilerplate=template_id
)
self.assertEqual(resp.status_code, 200)
prob_location = self.response_id(resp)
problem = modulestore('draft').get_item(prob_location)
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
......@@ -119,133 +120,102 @@ class TestCreateItem(CourseTestCase):
Negative tests for create_item
"""
# non-existent boilerplate: creates a default
resp = self.client.post(
reverse('create_item'),
json.dumps(
{'parent_location': self.course.location.url(),
'category': 'problem',
'boilerplate': 'nosuchboilerplate.yaml'
}),
content_type="application/json"
)
resp = self.create_xblock(category='problem', boilerplate='nosuchboilerplate.yaml')
self.assertEqual(resp.status_code, 200)
class TestEditItem(CourseTestCase):
class TestEditItem(ItemTest):
"""
Test contentstore.views.item.save_item
Test xblock update.
"""
def response_id(self, response):
"""
Get the id from the response payload
:param response:
"""
parsed = json.loads(response.content)
return parsed['id']
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.client.post(
reverse('create_item'),
json.dumps(
{'parent_location': self.course.location.url(),
'display_name': display_name,
'category': 'chapter'
}),
content_type="application/json"
)
chap_location = self.response_id(resp)
resp = self.client.post(
reverse('create_item'),
json.dumps({
'parent_location': chap_location,
'category': 'sequential',
}),
content_type="application/json"
)
self.seq_location = self.response_id(resp)
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.client.post(
reverse('create_item'),
json.dumps({
'parent_location': self.seq_location,
'category': 'problem',
'boilerplate': template_id,
}),
content_type="application/json"
)
self.problems = [self.response_id(resp)]
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.post(
reverse('save_item'),
json.dumps({
'id': self.problems[0],
'metadata': {'rerandomize': 'onreset'}
}),
content_type="application/json"
self.client.ajax_post(
self.problem_update_url,
data={'metadata': {'rerandomize': 'onreset'}}
)
problem = modulestore('draft').get_item(self.problems[0])
problem = self.get_item_from_modulestore(self.problem_locator, True)
self.assertEqual(problem.rerandomize, 'onreset')
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.problems[0],
'metadata': {'rerandomize': None}
}),
content_type="application/json"
self.client.ajax_post(
self.problem_update_url,
data={'metadata': {'rerandomize': None}}
)
problem = modulestore('draft').get_item(self.problems[0])
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 = modulestore('draft').get_item(self.problems[0])
problem = self.get_item_from_modulestore(self.problem_locator, True)
self.assertIsNotNone(problem.markdown)
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.problems[0],
'nullout': ['markdown']
}),
content_type="application/json"
self.client.ajax_post(
self.problem_update_url,
data={'nullout': ['markdown']}
)
problem = modulestore('draft').get_item(self.problems[0])
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 = modulestore().get_item(self.seq_location)
sequential = self.get_item_from_modulestore(self.seq_locator)
self.assertIsNone(sequential.due)
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.seq_location,
'metadata': {'due': '2010-11-22T04:00Z'}
}),
content_type="application/json"
self.client.ajax_post(
self.seq_update_url,
data={'metadata': {'due': '2010-11-22T04:00Z'}}
)
sequential = modulestore().get_item(self.seq_location)
sequential = self.get_item_from_modulestore(self.seq_locator)
self.assertEqual(sequential.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.seq_location,
'metadata': {'start': '2010-09-12T14:00Z'}
}),
content_type="application/json"
self.client.ajax_post(
self.seq_update_url,
data={'metadata': {'start': '2010-09-12T14:00Z'}}
)
sequential = modulestore().get_item(self.seq_location)
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_children(self):
# 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)
......@@ -19,6 +19,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore, _CONTENTSTORE
from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError
from xmodule.modulestore.django import loc_mapper
from contentstore.tests.modulestore_config import TEST_MODULESTORE
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
......@@ -47,14 +48,17 @@ class Basetranscripts(CourseTestCase):
def setUp(self):
"""Create initial data."""
super(Basetranscripts, self).setUp()
self.unicode_locator = unicode(loc_mapper().translate_location(
self.course.location.course_id, self.course.location, False, True
))
# Add video module
data = {
'parent_location': str(self.course_location),
'parent_locator': self.unicode_locator,
'category': 'video',
'type': 'video'
}
resp = self.client.ajax_post(reverse('create_item'), data)
resp = self.client.ajax_post('/xblock', data)
self.item_location = json.loads(resp.content).get('id')
self.assertEqual(resp.status_code, 200)
......@@ -196,11 +200,11 @@ class TestUploadtranscripts(Basetranscripts):
def test_fail_for_non_video_module(self):
# non_video module: setup
data = {
'parent_location': str(self.course_location),
'parent_locator': self.unicode_locator,
'category': 'non_video',
'type': 'non_video'
}
resp = self.client.ajax_post(reverse('create_item'), data)
resp = self.client.ajax_post('/xblock', data)
item_location = json.loads(resp.content).get('id')
data = '<non_video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M" />'
modulestore().update_item(item_location, data)
......@@ -407,11 +411,11 @@ class TestDownloadtranscripts(Basetranscripts):
def test_fail_for_non_video_module(self):
# Video module: setup
data = {
'parent_location': str(self.course_location),
'parent_locator': self.unicode_locator,
'category': 'videoalpha',
'type': 'videoalpha'
}
resp = self.client.ajax_post(reverse('create_item'), data)
resp = self.client.ajax_post('/xblock', data)
item_location = json.loads(resp.content).get('id')
subs_id = str(uuid4())
data = textwrap.dedent("""
......@@ -657,11 +661,11 @@ class TestChecktranscripts(Basetranscripts):
def test_fail_for_non_video_module(self):
# Not video module: setup
data = {
'parent_location': str(self.course_location),
'parent_locator': self.unicode_locator,
'category': 'not_video',
'type': 'not_video'
}
resp = self.client.ajax_post(reverse('create_item'), data)
resp = self.client.ajax_post('/xblock', data)
item_location = json.loads(resp.content).get('id')
subs_id = str(uuid4())
data = textwrap.dedent("""
......
......@@ -120,6 +120,10 @@ def edit_subsection(request, location):
can_view_live = True
break
locator = loc_mapper().translate_location(
course.location.course_id, item.location, False, True
)
return render_to_response(
'edit_subsection.html',
{
......@@ -129,8 +133,10 @@ def edit_subsection(request, location):
'lms_link': lms_link,
'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
# For grader, which is not yet converted
'parent_location': course.location,
'parent_item': parent,
'locator': locator,
'policy_metadata': policy_metadata,
'subsection_units': subsection_units,
'can_view_live': can_view_live
......@@ -175,9 +181,9 @@ def edit_unit(request, location):
# Note that the unit_state (draft, public, private) does not match up with the published value
# passed to translate_location. The two concepts are different at this point.
unit_update_url = loc_mapper().translate_location(
unit_locator = loc_mapper().translate_location(
course.location.course_id, Location(location), False, True
).url_reverse("xblock", "")
)
component_templates = defaultdict(list)
for category in COMPONENT_TYPES:
......@@ -247,7 +253,7 @@ def edit_unit(request, location):
component.location.url(),
loc_mapper().translate_location(
course.location.course_id, component.location, False, True
).url_reverse("xblock")
)
]
for component
in item.get_children()
......@@ -296,8 +302,9 @@ def edit_unit(request, location):
return render_to_response('unit.html', {
'context_course': course,
'unit': item,
# Still needed for creating a draft.
'unit_location': location,
'unit_update_url': unit_update_url,
'unit_locator': unit_locator,
'components': components,
'component_templates': component_templates,
'draft_preview_link': preview_lms_link,
......
......@@ -192,7 +192,9 @@ def course_index(request, course_id, branch, version_guid, block):
'course_graders': json.dumps(
CourseGradingModel.fetch(course.location).graders
),
# This is used by course grader, which has not yet been updated.
'parent_location': course.location,
'parent_locator': location,
'new_section_category': 'chapter',
'new_subsection_category': 'sequential',
'new_unit_category': 'vertical',
......
......@@ -7,7 +7,6 @@ from static_replace import replace_static_urls
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
......@@ -24,15 +23,18 @@ from xmodule.x_module import XModuleDescriptor
from django.views.decorators.http import require_http_methods
from xmodule.modulestore.locator import BlockUsageLocator
from student.models import CourseEnrollment
from django.http import HttpResponseBadRequest
from xblock.fields import Scope
__all__ = ['save_item', 'create_item', 'orphan', 'xblock_handler']
__all__ = ['orphan', 'xblock_handler']
log = logging.getLogger(__name__)
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
CREATE_IF_NOT_FOUND = ['course_info']
# pylint: disable=unused-argument
@require_http_methods(("DELETE", "GET", "PUT", "POST"))
......@@ -45,106 +47,118 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
DELETE
json: delete this xblock instance from the course. Supports query parameters "recurse" to delete
all children and "all_versions" to delete from all (mongo) versions.
GET
json: returns representation of the xblock (locator id, data, and metadata).
PUT or POST
json: if xblock location is specified, update the xblock instance. The json payload can contain
these fields, all optional:
:data: the new value for the data.
:children: the locator ids of children for this xblock.
:metadata: new values for the metadata fields. Any whose values are None will be deleted not set
to None! Absent ones will be left alone.
:nullout: which metadata fields to set to None
The JSON representation on the updated xblock (minus children) is returned.
if xblock location is not specified, create a new xblock instance. The json playload can contain
these fields:
:parent_locator: parent for new xblock, required
:category: type of xblock, required
:display_name: name for new xblock, optional
:boilerplate: template name for populating fields, optional
The locator (and old-style id) for the created xblock (minus children) is returned.
"""
location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, location):
raise PermissionDenied()
if request.method == 'GET':
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
rsp = _get_module_info(location, rewrite_static_links=rewrite_static_links)
return JsonResponse(rsp)
elif request.method in ("POST", "PUT"):
# Replace w/ save_item from below
rsp = _set_module_info(location, request.json)
return JsonResponse(rsp)
elif request.method == 'DELETE':
if course_id is not None:
location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, location):
raise PermissionDenied()
old_location = loc_mapper().translate_locator_to_location(location)
delete_children = bool(request.REQUEST.get('recurse', False))
delete_all_versions = bool(request.REQUEST.get('all_versions', False))
_delete_item_at_location(old_location, delete_children, delete_all_versions)
return JsonResponse()
if request.method == 'GET':
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
rsp = _get_module_info(location, rewrite_static_links=rewrite_static_links)
return JsonResponse(rsp)
elif request.method == 'DELETE':
delete_children = bool(request.REQUEST.get('recurse', False))
delete_all_versions = bool(request.REQUEST.get('all_versions', False))
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.
return _save_item(
location,
old_location,
data=request.json.get('data'),
children=request.json.get('children'),
metadata=request.json.get('metadata'),
nullout=request.json.get('nullout')
)
elif request.method in ('PUT', 'POST'):
return _create_item(request)
else:
return HttpResponseBadRequest(
"Only instance creation is supported without a course_id.",
content_type="text/plain"
)
@login_required
@expect_json
def save_item(request):
"""
Will carry a json payload with these possible fields
:id (required): the id
:data (optional): the new value for the data
:metadata (optional): new values for the metadata fields.
Any whose values are None will be deleted not set to None! Absent ones will be left alone
:nullout (optional): which metadata fields to set to None
def _save_item(usage_loc, item_location, data=None, children=None, metadata=None, nullout=None):
"""
# The nullout is a bit of a temporary copout until we can make module_edit.coffee and the metadata editors a
# little smarter and able to pass something more akin to {unset: [field, field]}
Saves certain properties (data, children, metadata, nullout) for a given xblock item.
try:
item_location = request.json['id']
except KeyError:
import inspect
log.exception(
'''Request missing required attribute 'id'.
Request info:
%s
Caller:
Function %s in file %s
''',
request.META,
inspect.currentframe().f_back.f_code.co_name,
inspect.currentframe().f_back.f_code.co_filename
)
return JsonResponse({"error": "Request missing required attribute 'id'."}, 400)
The item_location is still the old-style location.
"""
store = get_modulestore(item_location)
try:
old_item = modulestore().get_item(item_location)
except (ItemNotFoundError, InvalidLocationError):
existing_item = store.get_item(item_location)
except ItemNotFoundError:
if item_location.category in CREATE_IF_NOT_FOUND:
# New module at this location, for pages that are not pre-created.
# Used for course info handouts.
store.create_and_save_xmodule(item_location)
existing_item = store.get_item(item_location)
else:
raise
except InvalidLocationError:
log.error("Can't find item by location.")
return JsonResponse({"error": "Can't find item by location"}, 404)
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
store = get_modulestore(Location(item_location))
return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404)
if request.json.get('data'):
data = request.json['data']
if data:
store.update_item(item_location, data)
else:
data = existing_item.get_explicitly_set_fields_by_scope(Scope.content)
if request.json.get('children') is not None:
children = request.json['children']
store.update_children(item_location, children)
if children is not None:
children_ids = [
loc_mapper().translate_locator_to_location(BlockUsageLocator(child_locator)).url()
for child_locator
in children
]
store.update_children(item_location, children_ids)
# cdodge: also commit any metadata which might have been passed along
if request.json.get('nullout') is not None or request.json.get('metadata') is not None:
if nullout is not None or metadata is not None:
# the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
existing_item = modulestore().get_item(item_location)
for metadata_key in request.json.get('nullout', []):
setattr(existing_item, metadata_key, None)
# not presented to the end-user for editing. So let's use the original (existing_item) and
# 'apply' the submitted metadata, so we don't end up deleting system metadata.
if nullout is not None:
for metadata_key in nullout:
setattr(existing_item, metadata_key, None)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If
# the intent is to make it None, use the nullout field
for metadata_key, value in request.json.get('metadata', {}).items():
field = existing_item.fields[metadata_key]
if value is None:
field.delete_from(existing_item)
else:
try:
value = field.from_json(value)
except ValueError:
return JsonResponse({"error": "Invalid data"}, 400)
field.write_to(existing_item, value)
if metadata is not None:
for metadata_key, value in metadata.items():
field = existing_item.fields[metadata_key]
if value is None:
field.delete_from(existing_item)
else:
try:
value = field.from_json(value)
except ValueError:
return JsonResponse({"error": "Invalid data"}, 400)
field.write_to(existing_item, value)
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
......@@ -153,16 +167,23 @@ def save_item(request):
store.update_metadata(item_location, own_metadata(existing_item))
if existing_item.category == 'video':
manage_video_subtitles_save(old_item, existing_item)
manage_video_subtitles_save(existing_item, existing_item)
return JsonResponse()
# Note that children aren't returned because it is currently expensive to get the
# containing course for an xblock (and that is necessary to convert to locators).
return JsonResponse({
'id': unicode(usage_loc),
'data': data,
'metadata': own_metadata(existing_item)
})
@login_required
@expect_json
def create_item(request):
def _create_item(request):
"""View for create items."""
parent_location = Location(request.json['parent_location'])
parent_locator = BlockUsageLocator(request.json['parent_locator'])
parent_location = loc_mapper().translate_locator_to_location(parent_locator)
category = request.json['category']
display_name = request.json.get('display_name')
......@@ -171,7 +192,10 @@ def create_item(request):
raise PermissionDenied()
parent = get_modulestore(category).get_item(parent_location)
dest_location = parent_location.replace(category=category, name=uuid4().hex)
# Necessary to set revision=None or else metadata inheritance does not work
# (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
metadata = {}
......@@ -201,7 +225,7 @@ def create_item(request):
locator = loc_mapper().translate_location(
get_course_for_item(parent_location).location.course_id, dest_location, False, True
)
return JsonResponse({'id': dest_location.url(), "update_url": locator.url_reverse("xblock")})
return JsonResponse({'id': dest_location.url(), "locator": unicode(locator)})
def _delete_item_at_location(item_location, delete_children=False, delete_all_versions=False):
......@@ -232,6 +256,8 @@ def _delete_item_at_location(item_location, delete_children=False, delete_all_ve
parent.children = children
modulestore('direct').update_children(parent.location, parent.children)
return JsonResponse()
# pylint: disable=W0613
@login_required
......@@ -275,8 +301,8 @@ def _get_module_info(usage_loc, rewrite_static_links=False):
try:
module = store.get_item(old_location)
except ItemNotFoundError:
if old_location.category in ['course_info']:
# create a new one
if old_location.category in CREATE_IF_NOT_FOUND:
# Create a new one for certain categories only. Used for course info handouts.
store.create_and_save_xmodule(old_location)
module = store.get_item(old_location)
else:
......@@ -292,75 +318,10 @@ def _get_module_info(usage_loc, rewrite_static_links=False):
course_id=module.location.org + '/' + module.location.course + '/BOGUS_RUN_REPLACE_WHEN_AVAILABLE'
)
# Note that children aren't returned because it is currently expensive to get the
# containing course for an xblock (and that is necessary to convert to locators).
return {
'id': unicode(usage_loc),
'data': data,
'metadata': module.get_explicitly_set_fields_by_scope(Scope.settings)
}
def _set_module_info(usage_loc, post_data):
"""
Old metadata, data, id representation leaf module updater.
:param usage_loc: a BlockUsageLocator
:param post_data: the payload with data, metadata, and possibly children (even tho the getter
doesn't support children)
"""
# TODO replace with save_item: differences
# - this doesn't handle nullout
# - this returns the new model
old_location = loc_mapper().translate_locator_to_location(usage_loc)
store = get_modulestore(old_location)
module = None
try:
module = store.get_item(old_location)
except ItemNotFoundError:
# new module at this location: almost always used for the course about pages; thus, no parent. (there
# are quite a handful of about page types available for a course and only the overview is pre-created)
store.create_and_save_xmodule(old_location)
module = store.get_item(old_location)
if post_data.get('data') is not None:
data = post_data['data']
store.update_item(old_location, data)
else:
data = module.get_explicitly_set_fields_by_scope(Scope.content)
if post_data.get('children') is not None:
children = post_data['children']
store.update_children(old_location, children)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if post_data.get('metadata') is not None:
posted_metadata = post_data['metadata']
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
field = module.fields[metadata_key]
if value is None:
# remove both from passed in collection as well as the collection read in from the modulestore
field.delete_from(module)
else:
try:
value = field.from_json(value)
except ValueError:
return JsonResponse({"error": "Invalid data"}, 400)
field.write_to(module, value)
# commit to datastore
metadata = module.get_explicitly_set_fields_by_scope(Scope.settings)
store.update_metadata(old_location, metadata)
else:
metadata = module.get_explicitly_set_fields_by_scope(Scope.settings)
return {
'id': unicode(usage_loc),
'data': data,
'metadata': metadata
'metadata': own_metadata(module)
}
......@@ -13,6 +13,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator
from ..utils import get_course_for_item, get_modulestore
......@@ -47,8 +48,12 @@ def initialize_course_tabs(course):
@expect_json
def reorder_static_tabs(request):
"Order the static tabs in the requested order"
def get_location_for_tab(tab):
tab_locator = BlockUsageLocator(tab)
return loc_mapper().translate_locator_to_location(tab_locator)
tabs = request.json['tabs']
course = get_course_for_item(tabs[0])
course = get_course_for_item(get_location_for_tab(tabs[0]))
if not has_access(request.user, course.location):
raise PermissionDenied()
......@@ -64,7 +69,7 @@ def reorder_static_tabs(request):
# load all reference tabs, return BadRequest if we can't find any of them
tab_items = []
for tab in tabs:
item = modulestore('direct').get_item(Location(tab))
item = modulestore('direct').get_item(get_location_for_tab(tab))
if item is None:
return HttpResponseBadRequest()
......@@ -122,15 +127,20 @@ def edit_tabs(request, org, course, coursename):
static_tab.location.url(),
loc_mapper().translate_location(
course_item.location.course_id, static_tab.location, False, True
).url_reverse("xblock")
)
]
for static_tab
in static_tabs
]
course_locator = loc_mapper().translate_location(
course_item.location.course_id, course_item.location, False, True
)
return render_to_response('edit-tabs.html', {
'context_course': course_item,
'components': components
'components': components,
'locator': course_locator
})
......
......@@ -182,7 +182,7 @@ define([
"coffee/spec/main_spec",
"coffee/spec/models/course_spec", "coffee/spec/models/metadata_spec",
"coffee/spec/models/module_spec", "coffee/spec/models/section_spec",
"coffee/spec/models/section_spec",
"coffee/spec/models/settings_course_grader_spec",
"coffee/spec/models/settings_grading_spec", "coffee/spec/models/textbook_spec",
"coffee/spec/models/upload_spec",
......@@ -193,9 +193,11 @@ define([
"coffee/spec/views/overview_spec",
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
"js_spec/transcripts/utils_spec", "js_spec/transcripts/editor_spec",
"js_spec/transcripts/videolist_spec", "js_spec/transcripts/message_manager_spec",
"js_spec/transcripts/file_uploader_spec"
"js/spec/transcripts/utils_spec", "js/spec/transcripts/editor_spec",
"js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec",
"js/spec/transcripts/file_uploader_spec",
"js/spec/utils/module_spec"
# these tests are run separate in the cms-squire suite, due to process
# isolation issues with Squire.js
......
define ["coffee/src/models/module"], (Module) ->
describe "Module", ->
it "set the correct URL", ->
expect(new Module().url).toEqual("/save_item")
it "set the correct default", ->
expect(new Module().defaults).toEqual(undefined)
define ["js/models/section", "sinon"], (Section, sinon) ->
define ["js/models/section", "sinon", "js/utils/module"], (Section, sinon, ModuleUtils) ->
describe "Section", ->
describe "basic", ->
beforeEach ->
@model = new Section({
id: 42,
id: 42
name: "Life, the Universe, and Everything"
})
......@@ -14,11 +14,10 @@ define ["js/models/section", "sinon"], (Section, sinon) ->
expect(@model.get("name")).toEqual("Life, the Universe, and Everything")
it "should have a URL set", ->
expect(@model.url).toEqual("/save_item")
expect(@model.url()).toEqual(ModuleUtils.getUpdateUrl(42))
it "should serialize to JSON correctly", ->
expect(@model.toJSON()).toEqual({
id: 42,
metadata:
{
display_name: "Life, the Universe, and Everything"
......@@ -30,7 +29,7 @@ define ["js/models/section", "sinon"], (Section, sinon) ->
spyOn(Section.prototype, 'showNotification')
spyOn(Section.prototype, 'hideNotification')
@model = new Section({
id: 42,
id: 42
name: "Life, the Universe, and Everything"
})
@requests = requests = []
......
......@@ -4,6 +4,9 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
beforeEach ->
@stubModule = jasmine.createSpy("Module")
@stubModule.id = 'stub-id'
@stubModule.get = (param)->
if param == 'old_id'
return 'stub-old-id'
setFixtures """
<li class="component" id="stub-id">
......@@ -59,7 +62,7 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
@moduleEdit.render()
it "loads the module preview and editor via ajax on the view element", ->
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/preview_component/#{@moduleEdit.model.id}", jasmine.any(Function))
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/preview_component/#{@moduleEdit.model.get('old_id')}", jasmine.any(Function))
@moduleEdit.$el.load.mostRecentCall.args[1]()
expect(@moduleEdit.loadDisplay).toHaveBeenCalled()
expect(@moduleEdit.delegateEvents).toHaveBeenCalled()
......
......@@ -8,7 +8,7 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
<span class="published-status">
<strong>Will Release:</strong> 06/12/2013 at 04:00 UTC
</span>
<a href="#" class="edit-button" data-date="06/12/2013" data-time="04:00" data-id="i4x://pfogg/42/chapter/d6b47f7b084f49debcaf67fe5436c8e2">Edit</a>
<a href="#" class="edit-button" data-date="06/12/2013" data-time="04:00" data-locator="i4x://pfogg/42/chapter/d6b47f7b084f49debcaf67fe5436c8e2">Edit</a>
</div>
"""
......@@ -35,8 +35,8 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
"""
appendSetFixtures """
<section class="courseware-section branch" data-id="a-location-goes-here">
<li class="branch collapsed id-holder" data-id="an-id-goes-here">
<section class="courseware-section branch" data-locator="a-location-goes-here">
<li class="branch collapsed id-holder" data-id="an-id-goes-here" data-locator="an-id-goes-here">
<a href="#" class="delete-section-button"></a>
</li>
</section>
......@@ -44,19 +44,19 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
appendSetFixtures """
<ol>
<li class="subsection-list branch" data-id="subsection-1-id" id="subsection-1">
<li class="subsection-list branch" id="subsection-1" data-locator="subsection-1-id">
<ol class="sortable-unit-list" id="subsection-list-1">
<li class="unit" id="unit-1" data-id="first-unit-id" data-parent-id="subsection-1-id"></li>
<li class="unit" id="unit-2" data-id="second-unit-id" data-parent-id="subsection-1-id"></li>
<li class="unit" id="unit-3" data-id="third-unit-id" data-parent-id="subsection-1-id"></li>
<li class="unit" id="unit-1" data-parent="subsection-1-id" data-locator="first-unit-id"></li>
<li class="unit" id="unit-2" data-parent="subsection-1-id" data-locator="second-unit-id"></li>
<li class="unit" id="unit-3" data-parent="subsection-1-id" data-locator="third-unit-id"></li>
</ol>
</li>
<li class="subsection-list branch" data-id="subsection-2-id" id="subsection-2">
<li class="subsection-list branch" id="subsection-2" data-locator="subsection-2-id">
<ol class="sortable-unit-list" id="subsection-list-2">
<li class="unit" id="unit-4" data-id="fourth-unit-id" data-parent-id="subsection-2"></li>
<li class="unit" id="unit-4" data-parent="subsection-2" data-locator="fourth-unit-id"></li>
</ol>
</li>
<li class="subsection-list branch" data-id="subsection-3-id" id="subsection-3">
<li class="subsection-list branch" id="subsection-3" data-locator="subsection-3-id">
<ol class="sortable-unit-list" id="subsection-list-3">
</li>
</ol>
......@@ -366,10 +366,10 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
expect($('#unit-1')).toHaveClass('was-dropped')
# We expect 2 requests to be sent-- the first for removing Unit 1 from Subsection 1,
# and the second for adding Unit 1 to the end of Subsection 2.
expect(@requests[0].requestBody).toEqual('{"id":"subsection-1-id","children":["second-unit-id","third-unit-id"]}')
expect(@requests[0].requestBody).toEqual('{"children":["second-unit-id","third-unit-id"]}')
@requests[0].respond(200)
expect(@savingSpies.hide).not.toHaveBeenCalled()
expect(@requests[1].requestBody).toEqual('{"id":"subsection-2-id","children":["fourth-unit-id","first-unit-id"]}')
expect(@requests[1].requestBody).toEqual('{"children":["fourth-unit-id","first-unit-id"]}')
@requests[1].respond(200)
expect(@savingSpies.hide).toHaveBeenCalled()
# Class is removed in a timeout.
......
define ["backbone"], (Backbone) ->
class Module extends Backbone.Model
url: '/save_item'
......@@ -63,20 +63,21 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata())
createItem: (parent, payload) ->
payload.parent_location = parent
payload.parent_locator = parent
$.postJSON(
"/create_item"
@model.urlRoot
payload
(data) =>
@model.set(id: data.id)
@model.set(id: data.locator)
@model.set(old_id: data.id)
@$el.data('id', data.id)
@$el.data('update_url', data.update_url)
@$el.data('locator', data.locator)
@render()
)
render: ->
if @model.id
@$el.load("/preview_component/#{@model.id}", =>
if @model.get('old_id')
@$el.load("/preview_component/#{@model.get('old_id')}", =>
@loadDisplay()
@delegateEvents()
)
......
define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views/feedback_notification", "coffee/src/models/module", "coffee/src/views/module_edit"],
($, ui, Backbone, PromptView, NotificationView, ModuleModel, ModuleEditView) ->
define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views/feedback_notification",
"coffee/src/views/module_edit", "js/models/module_info", "js/utils/module"],
($, ui, Backbone, PromptView, NotificationView, ModuleEditView, ModuleModel, ModuleUtils) ->
class TabsEdit extends Backbone.View
initialize: =>
@$('.component').each((idx, element) =>
model = new ModuleModel({
id: $(element).data('locator'),
old_id:$(element).data('id')
})
new ModuleEditView(
el: element,
onDelete: @deleteTab,
model: new ModuleModel(
id: $(element).data('id'),
)
model: model
)
)
......@@ -28,7 +32,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
tabMoved: (event, ui) =>
tabs = []
@$('.component').each((idx, element) =>
tabs.push($(element).data('id'))
tabs.push($(element).data('locator'))
)
analytics.track "Reordered Static Pages",
......@@ -78,13 +82,13 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
analytics.track "Deleted Static Page",
course: course_location_analytics
id: $component.data('id')
id: $component.data('locator')
deleting = new NotificationView.Mini
title: gettext('Deleting&hellip;')
deleting.show()
$.ajax({
type: 'DELETE',
url: $component.data('update_url')
url: ModuleUtils.getUpdateUrl($component.data('locator'))
}).success(=>
$component.remove()
deleting.hide()
......
define ["jquery", "jquery.ui", "gettext", "backbone",
"js/views/feedback_notification", "js/views/feedback_prompt",
"coffee/src/models/module", "coffee/src/views/module_edit"],
($, ui, gettext, Backbone, NotificationView, PromptView, ModuleModel, ModuleEditView) ->
"coffee/src/views/module_edit", "js/models/module_info"],
($, ui, gettext, Backbone, NotificationView, PromptView, ModuleEditView, ModuleModel) ->
class UnitEditView extends Backbone.View
events:
'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates'
......@@ -61,11 +61,13 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
)
@$('.component').each (idx, element) =>
model = new ModuleModel
id: $(element).data('locator')
old_id: $(element).data('id')
new ModuleEditView
el: element,
onDelete: @deleteComponent,
model: new ModuleModel
id: $(element).data('id')
model: model
showComponentTemplates: (event) =>
event.preventDefault()
......@@ -96,7 +98,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@$newComponentItem.before(editor.$el)
editor.createItem(
@$el.data('id'),
@$el.data('locator'),
$(event.currentTarget).data()
)
......@@ -107,7 +109,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@closeNewComponent(event)
components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
components: => @$('.component').map((idx, el) -> $(el).data('locator')).get()
wait: (value) =>
@$('.unit-body').toggleClass("waiting", value)
......@@ -136,13 +138,13 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
$component = $(event.currentTarget).parents('.component')
$.ajax({
type: 'DELETE',
url: $component.data('update_url')
url: @model.urlRoot + "/" + $component.data('locator')
}).success(=>
deleting.hide()
analytics.track "Deleted a Component",
course: course_location_analytics
unit_id: unit_location_analytics
id: $component.data('id')
id: $component.data('locator')
$component.remove()
# b/c we don't vigilantly keep children up to date
......@@ -165,7 +167,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@wait(true)
$.ajax({
type: 'DELETE',
url: @$el.data('update_url') + "?" + $.param({recurse: true})
url: @model.urlRoot + "/" + @$el.data('locator') + "?" + $.param({recurse: true})
}).success(=>
analytics.track "Deleted Draft",
......
require(["domReady", "jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt",
"js/utils/get_date", "jquery.ui", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"],
function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils) {
"js/utils/get_date", "js/utils/module", "jquery.ui", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"],
function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, ModuleUtils) {
var $body;
var $newComponentItem;
......@@ -178,7 +178,7 @@ function saveSubsection() {
$spinner.show();
}
var id = $('.subsection-body').data('id');
var locator = $('.subsection-body').data('locator');
// pull all 'normalized' metadata editable fields on page
var metadata_fields = $('input[data-metadata-name]');
......@@ -202,12 +202,11 @@ function saveSubsection() {
});
$.ajax({
url: "/save_item",
type: "POST",
url: ModuleUtils.getUpdateUrl(locator),
type: "PUT",
dataType: "json",
contentType: "application/json",
data: JSON.stringify({
'id': id,
'metadata': metadata
}),
success: function() {
......@@ -226,12 +225,12 @@ function createNewUnit(e) {
analytics.track('Created a Unit', {
'course': course_location_analytics,
'parent_location': parent
'parent_locator': parent
});
$.postJSON('/create_item', {
'parent_location': parent,
$.postJSON(ModuleUtils.getUpdateUrl(), {
'parent_locator': parent,
'category': category,
'display_name': 'New Unit'
},
......@@ -267,11 +266,11 @@ function _deleteItem($el, type) {
click: function(view) {
view.hide();
var id = $el.data('id');
var locator = $el.data('locator');
analytics.track('Deleted an Item', {
'course': course_location_analytics,
'id': id
'id': locator
});
var deleting = new NotificationView.Mini({
......@@ -281,7 +280,7 @@ function _deleteItem($el, type) {
$.ajax({
type: 'DELETE',
url: $el.data('update_url')+'?'+ $.param({recurse: true, all_versions: true}),
url: ModuleUtils.getUpdateUrl(locator) +'?'+ $.param({recurse: true, all_versions: true}),
success: function () {
$el.remove();
deleting.hide();
......
define(["backbone"], function(Backbone) {
define(["backbone", "js/utils/module"], function(Backbone, ModuleUtils) {
var ModuleInfo = Backbone.Model.extend({
urlRoot: "/xblock",
urlRoot: ModuleUtils.urlRoot,
defaults: {
"id": null,
......
define(["backbone", "gettext", "js/views/feedback_notification"], function(Backbone, gettext, NotificationView) {
define(["backbone", "gettext", "js/views/feedback_notification", "js/utils/module"],
function(Backbone, gettext, NotificationView, ModuleUtils) {
var Section = Backbone.Model.extend({
defaults: {
"name": ""
......@@ -8,10 +10,9 @@ define(["backbone", "gettext", "js/views/feedback_notification"], function(Backb
return gettext("You must specify a name");
}
},
url: "/save_item",
urlRoot: ModuleUtils.urlRoot,
toJSON: function() {
return {
id: this.get("id"),
metadata: {
display_name: this.get("name")
}
......
define(['js/utils/module'],
function (ModuleUtils) {
describe('urlRoot ', function () {
it('defines xblock urlRoot', function () {
expect(ModuleUtils.urlRoot).toBe('/xblock');
});
});
describe('getUpdateUrl ', function () {
it('can take no arguments', function () {
expect(ModuleUtils.getUpdateUrl()).toBe('/xblock');
});
it('appends a locator', function () {
expect(ModuleUtils.getUpdateUrl("locator")).toBe('/xblock/locator');
});
});
}
);
/**
* Utilities for modules/xblocks.
*
* Returns:
*
* urlRoot: the root for creating/updating an xblock.
* getUpdateUrl: a utility method that returns the xblock update URL, appending
* the location if passed in.
*/
define([], function () {
var urlRoot = '/xblock';
var getUpdateUrl = function (locator) {
if (locator === undefined) {
return urlRoot;
}
else {
return urlRoot + "/" + locator;
}
};
return {
urlRoot: urlRoot,
getUpdateUrl: getUpdateUrl
};
});
define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification", "draggabilly",
"js/utils/modal", "js/utils/cancel_on_escape", "js/utils/get_date"],
function (domReady, $, ui, _, gettext, NotificationView, Draggabilly, ModalUtils, CancelOnEscape, DateUtils) {
"js/utils/modal", "js/utils/cancel_on_escape", "js/utils/get_date", "js/utils/module"],
function (domReady, $, ui, _, gettext, NotificationView, Draggabilly, ModalUtils, CancelOnEscape,
DateUtils, ModuleUtils) {
var modalSelector = '.edit-subsection-publish-settings';
......@@ -37,7 +38,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var editSectionPublishDate = function (e) {
e.preventDefault();
var $modal = $(modalSelector);
$modal.attr('data-id', $(this).attr('data-id'));
$modal.attr('data-locator', $(this).attr('data-locator'));
$modal.find('.start-date').val($(this).attr('data-date'));
$modal.find('.start-time').val($(this).attr('data-time'));
if ($modal.find('.start-date').val() == '' && $modal.find('.start-time').val() == '') {
......@@ -55,11 +56,11 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
$('.edit-subsection-publish-settings .start-time')
);
var id = $(modalSelector).attr('data-id');
var locator = $(modalSelector).attr('data-locator');
analytics.track('Edited Section Release Date', {
'course': course_location_analytics,
'id': id,
'id': locator,
'start': datetime
});
......@@ -69,12 +70,11 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
saving.show();
// call into server to commit the new order
$.ajax({
url: "/save_item",
type: "POST",
url: ModuleUtils.getUpdateUrl(locator),
type: "PUT",
dataType: "json",
contentType: "application/json",
data: JSON.stringify({
'id': id,
'metadata': {
'start': datetime
}
......@@ -86,18 +86,18 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
return (number < 10 ? '0' : '') + number;
};
var $thisSection = $('.courseware-section[data-id="' + id + '"]');
var $thisSection = $('.courseware-section[data-locator="' + locator + '"]');
var html = _.template(
'<span class="published-status">' +
'<strong>' + gettext("Will Release:") + '&nbsp;</strong>' +
gettext("{month}/{day}/{year} at {hour}:{minute} UTC") +
'</span>' +
'<a href="#" class="edit-button" data-date="{month}/{day}/{year}" data-time="{hour}:{minute}" data-id="{id}">' +
'<a href="#" class="edit-button" data-date="{month}/{day}/{year}" data-time="{hour}:{minute}" data-locator="{locator}">' +
gettext("Edit") +
'</a>',
{year: datetime.getUTCFullYear(), month: pad2(datetime.getUTCMonth() + 1), day: pad2(datetime.getUTCDate()),
hour: pad2(datetime.getUTCHours()), minute: pad2(datetime.getUTCMinutes()),
id: id},
locator: locator},
{interpolate: /\{(.+?)\}/g});
$thisSection.find('.section-published-date').html(html);
ModalUtils.hideModal();
......@@ -132,14 +132,14 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
'display_name': display_name
});
$.postJSON('/create_item', {
'parent_location': parent,
$.postJSON(ModuleUtils.getUpdateUrl(), {
'parent_locator': parent,
'category': category,
'display_name': display_name
},
function(data) {
if (data.id != undefined) location.reload();
if (data.locator != undefined) location.reload();
});
};
......@@ -159,7 +159,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var $saveButton = $newSubsection.find('.new-subsection-name-save');
var $cancelButton = $newSubsection.find('.new-subsection-name-cancel');
var parent = $(this).parents("section.branch").data("id");
var parent = $(this).parents("section.branch").data("locator");
$saveButton.data('parent', parent);
$saveButton.data('category', $(this).data('category'));
......@@ -182,14 +182,14 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
});
$.postJSON('/create_item', {
'parent_location': parent,
$.postJSON(ModuleUtils.getUpdateUrl(), {
'parent_locator': parent,
'category': category,
'display_name': display_name
},
function(data) {
if (data.id != undefined) {
if (data.locator != undefined) {
location.reload();
}
});
......@@ -219,7 +219,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
// Exclude the 'new unit' buttons, and make sure we don't
// prepend an element to itself
var siblings = container.children().filter(function () {
return $(this).data('id') !== undefined && !$(this).is(ele);
return $(this).data('locator') !== undefined && !$(this).is(ele);
});
// If the container is collapsed, check to see if the
// element is on top of its parent list -- don't check the
......@@ -416,16 +416,16 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var parentSelector = ele.data('parent-location-selector');
var childrenSelector = ele.data('child-selector');
var newParentEle = ele.parents(parentSelector).first();
var newParentID = newParentEle.data('id');
var oldParentID = ele.data('parent-id');
var newParentLocator = newParentEle.data('locator');
var oldParentLocator = ele.data('parent');
// If the parent has changed, update the children of the old parent.
if (oldParentID !== newParentID) {
if (newParentLocator !== oldParentLocator) {
// Find the old parent element.
var oldParentEle = $(parentSelector).filter(function () {
return $(this).data('id') === oldParentID;
return $(this).data('locator') === oldParentLocator;
});
this.saveItem(oldParentEle, childrenSelector, function () {
ele.data('parent-id', newParentID);
ele.data('parent', newParentLocator);
});
}
var saving = new NotificationView.Mini({
......@@ -452,16 +452,15 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var children = _.map(
ele.find(childrenSelector),
function (child) {
return $(child).data('id');
return $(child).data('locator');
}
);
$.ajax({
url: '/save_item',
type: 'POST',
url: ModuleUtils.getUpdateUrl(ele.data('locator')),
type: 'PUT',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({
id: ele.data('id'),
children: children
}),
success: success
......
......@@ -68,7 +68,7 @@ src_paths:
spec_paths:
- coffee/spec/main.js
- coffee/spec
- js_spec
- js/spec
# Paths to fixture files (optional)
# The fixture path will be set automatically when using jasmine-jquery.
......
......@@ -9,11 +9,11 @@
<%block name="jsextra">
<script type='text/javascript'>
require(["coffee/src/views/tabs", "coffee/src/models/module"], function(TabsEditView, ModuleModel) {
require(["backbone", "coffee/src/views/tabs"], function(Backbone, TabsEditView) {
new TabsEditView({
el: $('.main-wrapper'),
model: new ModuleModel({
id: '${context_course.location}'
model: new Backbone.Model({
id: '${locator}'
}),
mast: $('.wrapper-mast')
});
......@@ -61,8 +61,8 @@ require(["coffee/src/views/tabs", "coffee/src/models/module"], function(TabsEdit
<div class="tab-list">
<ol class='components'>
% for id, update_url in components:
<li class="component" data-id="${id}" data-update_url="${update_url}"/>
% for id, locator in components:
<li class="component" data-id="${id}" data-locator="${locator}"/>
% endfor
<li class="new-component-item">
......
......@@ -16,7 +16,7 @@
<div class="main-wrapper">
<div class="inner-wrapper">
<div class="main-column">
<article class="subsection-body window" data-id="${subsection.location}">
<article class="subsection-body window" data-locator="${locator}">
<div class="subsection-name-input">
<label>${_("Display Name:")}</label>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
......
......@@ -39,7 +39,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
$(".section-name").each(function() {
var model = new SectionModel({
id: $(this).parent(".item-details").data("id"),
id: $(this).parent(".item-details").data("locator"),
name: $(this).data("name")
});
new SectionShowView({model: model, el: this}).render();
......@@ -57,7 +57,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<h3 class="section-name">
<form class="section-name-form">
<input type="text" value="${_('New Section Name')}" class="new-section-name" />
<input type="submit" class="new-section-name-save" data-parent="${parent_location}"
<input type="submit" class="new-section-name-save" data-parent="${parent_locator}"
data-category="${new_section_category}" value="${_('Save')}" />
<input type="button" class="new-section-name-cancel" value="${_('Cancel')}" /></h3>
</form>
......@@ -75,7 +75,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<span class="section-name-span">Click here to set the section name</span>
<form class="section-name-form">
<input type="text" value="${_('New Section Name')}" class="new-section-name" />
<input type="submit" class="new-section-name-save" data-parent="${parent_location}"
<input type="submit" class="new-section-name-save" data-parent="${parent_locator}"
data-category="${new_section_category}" value="${_('Save')}" />
<input type="button" class="new-section-name-cancel" value="$(_('Cancel')}" /></h3>
</form>
......@@ -140,22 +140,26 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<div class="inner-wrapper">
<div class="wrapper-dnd">
<article class="courseware-overview" data-id="${context_course.location.url()}">
<%
course_locator = loc_mapper().translate_location(
context_course.location.course_id, context_course.location, False, True
)
%>
<article class="courseware-overview" data-locator="${course_locator}">
% for section in sections:
<%
section_update_url = loc_mapper().translate_location(
section_locator = loc_mapper().translate_location(
context_course.location.course_id, section.location, False, True
).url_reverse('xblock')
)
%>
<section class="courseware-section branch is-draggable" data-id="${section.location}"
data-parent-id="${context_course.location.url()}" data-update_url="${section_update_url}">
<section class="courseware-section branch is-draggable" data-parent="${course_locator}"
data-locator="${section_locator}">
<%include file="widgets/_ui-dnd-indicator-before.html" />
<header>
<a href="#" data-tooltip="${_('Expand/collapse this section')}" class="expand-collapse-icon collapse"></a>
<div class="item-details" data-id="${section.location}">
<div class="item-details" data-locator="${section_locator}">
<h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
<div class="section-published-date">
<%
......@@ -168,12 +172,12 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
%>
%if section.start is None:
<span class="published-status">${_("This section has not been released.")}</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">${_("Schedule")}</a>
<a href="#" class="schedule-button" data-date="" data-time="" data-locator="${section_locator}">${_("Schedule")}</a>
%else:
<span class="published-status"><strong>${_("Will Release:")}</strong>
${date_utils.get_default_time_display(section.start)}</span>
<a href="#" class="edit-button" data-date="${start_date_str}"
data-time="${start_time_str}" data-id="${section.location}">${_("Edit")}</a>
data-time="${start_time_str}" data-locator="${section_locator}">${_("Edit")}</a>
%endif
</div>
</div>
......@@ -189,15 +193,15 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<span class="new-folder-icon"></span>${_("New Subsection")}
</a>
</div>
<ol class="sortable-subsection-list" data-id="${section.location.url()}">
<ol class="sortable-subsection-list">
% for subsection in section.get_children():
<%
subsection_update_url = loc_mapper().translate_location(
subsection_locator = loc_mapper().translate_location(
context_course.location.course_id, subsection.location, False, True
).url_reverse('xblock')
)
%>
<li class="courseware-subsection branch collapsed id-holder is-draggable" data-id="${subsection.location}"
data-parent-id="${section.location.url()}" data-update_url="${subsection_update_url}">
data-parent="${section_locator}" data-locator="${subsection_locator}">
<%include file="widgets/_ui-dnd-indicator-before.html" />
......
......@@ -24,7 +24,6 @@ CMS.URL.LMS_BASE = "${settings.LMS_BASE}"
require(["js/models/section", "js/collections/textbook", "js/views/list_textbooks"],
function(Section, TextbookCollection, ListTextbooksView) {
window.section = new Section({
id: "${course.id}",
name: "${course.display_name_with_default | h}",
url_name: "${course.location.name | h}",
org: "${course.location.org | h}",
......
......@@ -10,9 +10,9 @@ from xmodule.modulestore.django import loc_mapper
<%block name="jsextra">
<script type='text/javascript'>
require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/unit", "jquery.ui"],
require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit", "jquery.ui"],
function(doc, $, ModuleModel, UnitEditView, ui) {
window.unit_location_analytics = '${unit_location}';
window.unit_location_analytics = '${unit_locator}';
// tabs
$('.tab-group').tabs();
......@@ -20,7 +20,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
new UnitEditView({
el: $('.main-wrapper'),
model: new ModuleModel({
id: '${unit_location}',
id: '${unit_locator}',
state: '${unit_state}'
})
});
......@@ -34,7 +34,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
</%block>
<%block name="content">
<div class="main-wrapper edit-state-${unit_state}" data-id="${unit_location}" data-update_url="${unit_update_url}">
<div class="main-wrapper edit-state-${unit_state}" data-id="${unit_location}" data-locator="${unit_locator}">
<div class="inner-wrapper">
<div class="alert editing-draft-alert">
<p class="alert-message"><strong>${_("You are editing a draft.")}</strong>
......@@ -48,8 +48,8 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
<article class="unit-body window">
<p class="unit-name-input"><label>${_("Display Name:")}</label><input type="text" value="${unit.display_name_with_default | h}" class="unit-display-name-input" /></p>
<ol class="components">
% for id, component_update_url in components:
<li class="component" data-id="${id}" data-update_url="${component_update_url}"/>
% for id, locator in components:
<li class="component" data-id="${id}" data-locator="${locator}"/>
% endfor
<li class="new-component-item adding">
<div class="new-component">
......@@ -141,7 +141,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
<div class="window-contents">
<div class="row visibility">
<label class="inline-label">${_("Visibility:")}</label>
<select class='visibility-select'>
<select name="visibility-select" class='visibility-select'>
<option value="public">${_("Public")}</option>
<option value="private">${_("Private")}</option>
</select>
......
......@@ -11,12 +11,15 @@ This def will enumerate through a passed in subsection and list all of the units
if subsection_units is None:
subsection_units = subsection.get_children()
%>
<%
subsection_locator = loc_mapper().translate_location(context_course.location.course_id, subsection.location, False, True)
%>
% for unit in subsection_units:
<%
unit_update_url = loc_mapper().translate_location(context_course.location.course_id, unit.location, False, True).url_reverse('xblock')
unit_locator = loc_mapper().translate_location(context_course.location.course_id, unit.location, False, True)
%>
<li class="courseware-unit leaf unit is-draggable" data-id="${unit.location}" data-parent-id="${subsection.location.url()}"
data-update_url="${unit_update_url}" >
<li class="courseware-unit leaf unit is-draggable" data-locator="${unit_locator}"
data-parent="${subsection_locator}">
<%include file="_ui-dnd-indicator-before.html" />
......@@ -34,8 +37,7 @@ This def will enumerate through a passed in subsection and list all of the units
</a>
% if actions:
<div class="item-actions">
<a href="#" data-tooltip="Delete this unit" class="delete-button" data-id="${unit.location}"
data-update_url="${unit_update_url}">
<a href="#" data-tooltip="Delete this unit" class="delete-button" data-locator="${unit_locator}">
<span class="delete-icon"></span></a>
<span data-tooltip="Drag to sort" class="drag-handle unit-drag-handle"></span>
</div>
......@@ -48,7 +50,7 @@ This def will enumerate through a passed in subsection and list all of the units
<li>
<%include file="_ui-dnd-indicator-initial.html" />
<a href="#" class="new-unit-item" data-category="${new_unit_category}" data-parent="${subsection.location}">
<a href="#" class="new-unit-item" data-category="${new_unit_category}" data-parent="${subsection_locator}">
<span class="new-unit-icon"></span>New Unit
</a>
</li>
......
......@@ -15,8 +15,6 @@ 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'^preview_component/(?P<location>.*?)$', 'contentstore.views.preview_component', name='preview_component'),
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
url(r'^create_item$', 'contentstore.views.create_item', name='create_item'),
url(r'^transcripts/upload$', 'contentstore.views.upload_transcripts', name='upload_transcripts'),
url(r'^transcripts/download$', 'contentstore.views.download_transcripts', name='download_transcripts'),
......@@ -115,7 +113,7 @@ urlpatterns += patterns(
url(r'(?ix)^import/{}$'.format(parsers.URL_RE_SOURCE), 'import_handler'),
url(r'(?ix)^import_status/{}/(?P<filename>.+)$'.format(parsers.URL_RE_SOURCE), 'import_status_handler'),
url(r'(?ix)^export/{}$'.format(parsers.URL_RE_SOURCE), 'export_handler'),
url(r'(?ix)^xblock/{}$'.format(parsers.URL_RE_SOURCE), 'xblock_handler'),
url(r'(?ix)^xblock($|/){}$'.format(parsers.URL_RE_SOURCE), 'xblock_handler'),
)
js_info_dict = {
......
......@@ -31,7 +31,7 @@ REQUIREJS_WAIT = {
# Individual Unit (editing)
re.compile('^Individual Unit \|'): [
"js/base", "coffee/src/models/module", "coffee/src/views/unit",
"js/base", "coffee/src/views/unit",
"coffee/src/views/module_edit"],
# Content - Outline
......
......@@ -184,12 +184,21 @@ class DraftModuleStore(MongoModuleStore):
location: Something that can be passed to Location
children: A list of child item identifiers
"""
# We expect the children IDs to always be the non-draft version. With view refactoring
# for split, we are now passing the draft version in some cases.
children_ids = [
Location(child).replace(revision=None).url()
for child
in children
]
draft_loc = as_draft(location)
draft_item = self.get_item(location)
if not getattr(draft_item, 'is_draft', False):
self.convert_to_draft(as_published(location))
return super(DraftModuleStore, self).update_children(draft_loc, children)
return super(DraftModuleStore, self).update_children(draft_loc, children_ids)
def update_metadata(self, location, metadata):
"""
......
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