Change save_item and create_item to RESTful URL.
Part of STUD-847.
This commit is contained in:
@@ -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))
|
||||
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))
|
||||
|
||||
_delete_item_at_location(old_location, delete_children, delete_all_versions)
|
||||
return JsonResponse()
|
||||
|
||||
|
||||
@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
|
||||
"""
|
||||
# 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]}
|
||||
|
||||
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 _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"
|
||||
)
|
||||
return JsonResponse({"error": "Request missing required attribute 'id'."}, 400)
|
||||
|
||||
|
||||
def _save_item(usage_loc, item_location, data=None, children=None, metadata=None, nullout=None):
|
||||
"""
|
||||
Saves certain properties (data, children, metadata, nullout) for a given xblock item.
|
||||
|
||||
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)
|
||||
return JsonResponse({"error": "Can't find item by location: " + str(item_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))
|
||||
|
||||
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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
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)
|
||||
@@ -1,9 +1,9 @@
|
||||
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.
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
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()
|
||||
)
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
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…')
|
||||
deleting.show()
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: $component.data('update_url')
|
||||
url: ModuleUtils.getUpdateUrl($component.data('locator'))
|
||||
}).success(=>
|
||||
$component.remove()
|
||||
deleting.hide()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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")
|
||||
}
|
||||
|
||||
17
cms/static/js/spec/utils/module_spec.js
Normal file
17
cms/static/js/spec/utils/module_spec.js
Normal file
@@ -0,0 +1,17 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
26
cms/static/js/utils/module.js
Normal file
26
cms/static/js/utils/module.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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:") + ' </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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user