Commit e2926a94 by Christina Roberts

Merge pull request #1621 from edx/christina/item-create

Change save_item and create_item to be RESTful.
parents 81e1d18d 569c86de
...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Studio: change create_item, delete_item, and save_item to RESTful API (STUD-847).
Blades: Fix answer choices rearranging if user tries to stylize something in the Blades: Fix answer choices rearranging if user tries to stylize something in the
text like with bold or italics. (BLD-449) text like with bold or italics. (BLD-449)
......
...@@ -385,3 +385,18 @@ def create_other_user(_step, name, has_extra_perms, role_name): ...@@ -385,3 +385,18 @@ def create_other_user(_step, name, has_extra_perms, role_name):
@step('I log out') @step('I log out')
def log_out(_step): def log_out(_step):
world.visit('logout') 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): ...@@ -87,13 +87,18 @@ def add_component_category(step, component, category):
@step(u'I delete all components$') @step(u'I delete all components$')
def delete_all_components(step): 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() world.wait_for_xmodule()
delete_btn_css = 'a.delete-button' delete_btn_css = 'a.delete-button'
prompt_css = 'div#prompt-warning' prompt_css = 'div#prompt-warning'
btn_css = '{} a.button.action-primary'.format(prompt_css) btn_css = '{} a.button.action-primary'.format(prompt_css)
saving_mini_css = 'div#page-notification .wrapper-notification-mini' saving_mini_css = 'div#page-notification .wrapper-notification-mini'
count = len(world.css_find('ol.components li.component')) for _ in range(int(number)):
for _ in range(int(count)):
world.css_click(delete_btn_css) world.css_click(delete_btn_css)
assert_true( assert_true(
world.is_css_present('{}.is-shown'.format(prompt_css)), world.is_css_present('{}.is-shown'.format(prompt_css)),
......
...@@ -81,6 +81,21 @@ Feature: CMS.Problem Editor ...@@ -81,6 +81,21 @@ Feature: CMS.Problem Editor
When I edit and select Settings When I edit and select Settings
Then Edit High Level Source is visible 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 # Disabled 11/13/2013 after failing in master
# The screenshot showed that the LaTeX editor had the text "hi", # The screenshot showed that the LaTeX editor had the text "hi",
# but Selenium timed out waiting for the text to appear. # but Selenium timed out waiting for the text to appear.
......
...@@ -19,6 +19,11 @@ SHOW_ANSWER = "Show Answer" ...@@ -19,6 +19,11 @@ SHOW_ANSWER = "Show Answer"
@step('I have created a Blank Common Problem$') @step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step): def i_created_blank_common_problem(step):
world.create_course_with_unit() 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( world.create_component_instance(
step=step, step=step,
category='problem', category='problem',
...@@ -218,11 +223,6 @@ def i_import_the_file(_step, filename): ...@@ -218,11 +223,6 @@ def i_import_the_file(_step, filename):
import_file(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 "([^"]*)"$') @step(u'I go to the vertical "([^"]*)"$')
def i_go_to_vertical(_step, vertical): def i_go_to_vertical(_step, vertical):
world.css_click("span:contains('{0}')".format(vertical)) world.css_click("span:contains('{0}')".format(vertical))
......
...@@ -398,9 +398,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -398,9 +398,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(course.tabs, expected_tabs) self.assertEqual(course.tabs, expected_tabs)
def test_static_tab_reordering(self): 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') module_store = modulestore('direct')
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course') locator = _course_factory_create_course()
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None]) course_location = loc_mapper().translate_locator_to_location(locator)
ItemFactory.create( ItemFactory.create(
parent_location=course_location, parent_location=course_location,
...@@ -411,23 +417,23 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -411,23 +417,23 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
category="static_tab", category="static_tab",
display_name="Static_2") 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 the ordering
reverse_tabs = [] reverse_tabs = []
for tab in course.tabs: for tab in course.tabs:
if tab['type'] == 'static_tab': 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}) 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 # compare to make sure that the tabs information is in the expected order after the server call
course_tabs = [] course_tabs = []
for tab in course.tabs: for tab in course.tabs:
if tab['type'] == 'static_tab': 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) self.assertEqual(reverse_tabs, course_tabs)
...@@ -1528,22 +1534,22 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1528,22 +1534,22 @@ class ContentStoreTest(ModuleStoreTestCase):
resp = self._show_course_overview(loc) resp = self._show_course_overview(loc)
self.assertContains( self.assertContains(
resp, 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, status_code=200,
html=True html=True
) )
def test_create_item(self): def test_create_item(self):
"""Test cloning an item. E.g. creating a new section""" """Test creating a new xblock instance."""
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') locator = _course_factory_create_course()
section_data = { section_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course', 'parent_locator': unicode(locator),
'category': 'chapter', 'category': 'chapter',
'display_name': 'Section One', '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) self.assertEqual(resp.status_code, 200)
data = parse_json(resp) data = parse_json(resp)
...@@ -1554,14 +1560,14 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1554,14 +1560,14 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_capa_module(self): def test_capa_module(self):
"""Test that a problem treats markdown specially.""" """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 = { problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course', 'parent_locator': unicode(locator),
'category': 'problem' '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) self.assertEqual(resp.status_code, 200)
payload = parse_json(resp) payload = parse_json(resp)
...@@ -1911,7 +1917,7 @@ class MetadataSaveTestCase(ModuleStoreTestCase): ...@@ -1911,7 +1917,7 @@ class MetadataSaveTestCase(ModuleStoreTestCase):
def _create_course(test, course_data): 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) course_id = _get_course_id(course_data)
new_location = loc_mapper().translate_location(course_id, CourseDescriptor.id_to_location(course_id), False, True) 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): ...@@ -1923,6 +1929,14 @@ def _create_course(test, course_data):
test.assertEqual(data['url'], new_location.url_reverse("course/", "")) 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): def _get_course_id(test_course_data):
"""Returns the course ID (org/number/run).""" """Returns the course ID (org/number/run)."""
return "{org}/{number}/{run}".format(**test_course_data) return "{org}/{number}/{run}".format(**test_course_data)
...@@ -19,6 +19,7 @@ from xmodule.modulestore.django import modulestore ...@@ -19,6 +19,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore, _CONTENTSTORE from xmodule.contentstore.django import contentstore, _CONTENTSTORE
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from xmodule.modulestore.django import loc_mapper
from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.tests.modulestore_config import TEST_MODULESTORE
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
...@@ -47,14 +48,17 @@ class Basetranscripts(CourseTestCase): ...@@ -47,14 +48,17 @@ class Basetranscripts(CourseTestCase):
def setUp(self): def setUp(self):
"""Create initial data.""" """Create initial data."""
super(Basetranscripts, self).setUp() 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 # Add video module
data = { data = {
'parent_location': str(self.course_location), 'parent_locator': self.unicode_locator,
'category': 'video', 'category': 'video',
'type': '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.item_location = json.loads(resp.content).get('id')
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
...@@ -196,11 +200,11 @@ class TestUploadtranscripts(Basetranscripts): ...@@ -196,11 +200,11 @@ class TestUploadtranscripts(Basetranscripts):
def test_fail_for_non_video_module(self): def test_fail_for_non_video_module(self):
# non_video module: setup # non_video module: setup
data = { data = {
'parent_location': str(self.course_location), 'parent_locator': self.unicode_locator,
'category': 'non_video', 'category': 'non_video',
'type': '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') item_location = json.loads(resp.content).get('id')
data = '<non_video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M" />' data = '<non_video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M" />'
modulestore().update_item(item_location, data) modulestore().update_item(item_location, data)
...@@ -407,11 +411,11 @@ class TestDownloadtranscripts(Basetranscripts): ...@@ -407,11 +411,11 @@ class TestDownloadtranscripts(Basetranscripts):
def test_fail_for_non_video_module(self): def test_fail_for_non_video_module(self):
# Video module: setup # Video module: setup
data = { data = {
'parent_location': str(self.course_location), 'parent_locator': self.unicode_locator,
'category': 'videoalpha', 'category': 'videoalpha',
'type': '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') item_location = json.loads(resp.content).get('id')
subs_id = str(uuid4()) subs_id = str(uuid4())
data = textwrap.dedent(""" data = textwrap.dedent("""
...@@ -657,11 +661,11 @@ class TestChecktranscripts(Basetranscripts): ...@@ -657,11 +661,11 @@ class TestChecktranscripts(Basetranscripts):
def test_fail_for_non_video_module(self): def test_fail_for_non_video_module(self):
# Not video module: setup # Not video module: setup
data = { data = {
'parent_location': str(self.course_location), 'parent_locator': self.unicode_locator,
'category': 'not_video', 'category': 'not_video',
'type': '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') item_location = json.loads(resp.content).get('id')
subs_id = str(uuid4()) subs_id = str(uuid4())
data = textwrap.dedent(""" data = textwrap.dedent("""
......
...@@ -120,6 +120,10 @@ def edit_subsection(request, location): ...@@ -120,6 +120,10 @@ def edit_subsection(request, location):
can_view_live = True can_view_live = True
break break
locator = loc_mapper().translate_location(
course.location.course_id, item.location, False, True
)
return render_to_response( return render_to_response(
'edit_subsection.html', 'edit_subsection.html',
{ {
...@@ -129,8 +133,10 @@ def edit_subsection(request, location): ...@@ -129,8 +133,10 @@ def edit_subsection(request, location):
'lms_link': lms_link, 'lms_link': lms_link,
'preview_link': preview_link, 'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders), 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
# For grader, which is not yet converted
'parent_location': course.location, 'parent_location': course.location,
'parent_item': parent, 'parent_item': parent,
'locator': locator,
'policy_metadata': policy_metadata, 'policy_metadata': policy_metadata,
'subsection_units': subsection_units, 'subsection_units': subsection_units,
'can_view_live': can_view_live 'can_view_live': can_view_live
...@@ -175,9 +181,9 @@ def edit_unit(request, location): ...@@ -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 # 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. # 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 course.location.course_id, Location(location), False, True
).url_reverse("xblock", "") )
component_templates = defaultdict(list) component_templates = defaultdict(list)
for category in COMPONENT_TYPES: for category in COMPONENT_TYPES:
...@@ -247,7 +253,7 @@ def edit_unit(request, location): ...@@ -247,7 +253,7 @@ def edit_unit(request, location):
component.location.url(), component.location.url(),
loc_mapper().translate_location( loc_mapper().translate_location(
course.location.course_id, component.location, False, True course.location.course_id, component.location, False, True
).url_reverse("xblock") )
] ]
for component for component
in item.get_children() in item.get_children()
...@@ -296,8 +302,9 @@ def edit_unit(request, location): ...@@ -296,8 +302,9 @@ def edit_unit(request, location):
return render_to_response('unit.html', { return render_to_response('unit.html', {
'context_course': course, 'context_course': course,
'unit': item, 'unit': item,
# Still needed for creating a draft.
'unit_location': location, 'unit_location': location,
'unit_update_url': unit_update_url, 'unit_locator': unit_locator,
'components': components, 'components': components,
'component_templates': component_templates, 'component_templates': component_templates,
'draft_preview_link': preview_lms_link, 'draft_preview_link': preview_lms_link,
......
...@@ -192,7 +192,9 @@ def course_index(request, course_id, branch, version_guid, block): ...@@ -192,7 +192,9 @@ def course_index(request, course_id, branch, version_guid, block):
'course_graders': json.dumps( 'course_graders': json.dumps(
CourseGradingModel.fetch(course.location).graders CourseGradingModel.fetch(course.location).graders
), ),
# This is used by course grader, which has not yet been updated.
'parent_location': course.location, 'parent_location': course.location,
'parent_locator': location,
'new_section_category': 'chapter', 'new_section_category': 'chapter',
'new_subsection_category': 'sequential', 'new_subsection_category': 'sequential',
'new_unit_category': 'vertical', 'new_unit_category': 'vertical',
......
...@@ -13,8 +13,9 @@ from xmodule.modulestore import Location ...@@ -13,8 +13,9 @@ from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator
from ..utils import get_course_for_item, get_modulestore from ..utils import get_modulestore
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -47,12 +48,18 @@ def initialize_course_tabs(course): ...@@ -47,12 +48,18 @@ def initialize_course_tabs(course):
@expect_json @expect_json
def reorder_static_tabs(request): def reorder_static_tabs(request):
"Order the static tabs in the requested order" "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'] tabs = request.json['tabs']
course = get_course_for_item(tabs[0]) course_location = loc_mapper().translate_locator_to_location(BlockUsageLocator(tabs[0]), get_course=True)
if not has_access(request.user, course.location): if not has_access(request.user, course_location):
raise PermissionDenied() raise PermissionDenied()
course = get_modulestore(course_location).get_item(course_location)
# get list of existing static tabs in course # get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number # make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we can drop some! # that we know about) otherwise we can drop some!
...@@ -64,7 +71,7 @@ def reorder_static_tabs(request): ...@@ -64,7 +71,7 @@ def reorder_static_tabs(request):
# load all reference tabs, return BadRequest if we can't find any of them # load all reference tabs, return BadRequest if we can't find any of them
tab_items = [] tab_items = []
for tab in tabs: 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: if item is None:
return HttpResponseBadRequest() return HttpResponseBadRequest()
...@@ -122,15 +129,20 @@ def edit_tabs(request, org, course, coursename): ...@@ -122,15 +129,20 @@ def edit_tabs(request, org, course, coursename):
static_tab.location.url(), static_tab.location.url(),
loc_mapper().translate_location( loc_mapper().translate_location(
course_item.location.course_id, static_tab.location, False, True course_item.location.course_id, static_tab.location, False, True
).url_reverse("xblock") )
] ]
for static_tab for static_tab
in static_tabs 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', { return render_to_response('edit-tabs.html', {
'context_course': course_item, 'context_course': course_item,
'components': components 'components': components,
'locator': course_locator
}) })
......
...@@ -182,7 +182,7 @@ define([ ...@@ -182,7 +182,7 @@ define([
"coffee/spec/main_spec", "coffee/spec/main_spec",
"coffee/spec/models/course_spec", "coffee/spec/models/metadata_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_course_grader_spec",
"coffee/spec/models/settings_grading_spec", "coffee/spec/models/textbook_spec", "coffee/spec/models/settings_grading_spec", "coffee/spec/models/textbook_spec",
"coffee/spec/models/upload_spec", "coffee/spec/models/upload_spec",
...@@ -193,9 +193,11 @@ define([ ...@@ -193,9 +193,11 @@ define([
"coffee/spec/views/overview_spec", "coffee/spec/views/overview_spec",
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec", "coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
"js_spec/transcripts/utils_spec", "js_spec/transcripts/editor_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/videolist_spec", "js/spec/transcripts/message_manager_spec",
"js_spec/transcripts/file_uploader_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 # these tests are run separate in the cms-squire suite, due to process
# isolation issues with Squire.js # 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 "Section", ->
describe "basic", -> describe "basic", ->
beforeEach -> beforeEach ->
@model = new Section({ @model = new Section({
id: 42, id: 42
name: "Life, the Universe, and Everything" name: "Life, the Universe, and Everything"
}) })
...@@ -14,11 +14,10 @@ define ["js/models/section", "sinon"], (Section, sinon) -> ...@@ -14,11 +14,10 @@ define ["js/models/section", "sinon"], (Section, sinon) ->
expect(@model.get("name")).toEqual("Life, the Universe, and Everything") expect(@model.get("name")).toEqual("Life, the Universe, and Everything")
it "should have a URL set", -> 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", -> it "should serialize to JSON correctly", ->
expect(@model.toJSON()).toEqual({ expect(@model.toJSON()).toEqual({
id: 42,
metadata: metadata:
{ {
display_name: "Life, the Universe, and Everything" display_name: "Life, the Universe, and Everything"
...@@ -30,7 +29,7 @@ define ["js/models/section", "sinon"], (Section, sinon) -> ...@@ -30,7 +29,7 @@ define ["js/models/section", "sinon"], (Section, sinon) ->
spyOn(Section.prototype, 'showNotification') spyOn(Section.prototype, 'showNotification')
spyOn(Section.prototype, 'hideNotification') spyOn(Section.prototype, 'hideNotification')
@model = new Section({ @model = new Section({
id: 42, id: 42
name: "Life, the Universe, and Everything" name: "Life, the Universe, and Everything"
}) })
@requests = requests = [] @requests = requests = []
......
...@@ -4,6 +4,9 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) -> ...@@ -4,6 +4,9 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
beforeEach -> beforeEach ->
@stubModule = jasmine.createSpy("Module") @stubModule = jasmine.createSpy("Module")
@stubModule.id = 'stub-id' @stubModule.id = 'stub-id'
@stubModule.get = (param)->
if param == 'old_id'
return 'stub-old-id'
setFixtures """ setFixtures """
<li class="component" id="stub-id"> <li class="component" id="stub-id">
...@@ -59,7 +62,7 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) -> ...@@ -59,7 +62,7 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
@moduleEdit.render() @moduleEdit.render()
it "loads the module preview and editor via ajax on the view element", -> 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]() @moduleEdit.$el.load.mostRecentCall.args[1]()
expect(@moduleEdit.loadDisplay).toHaveBeenCalled() expect(@moduleEdit.loadDisplay).toHaveBeenCalled()
expect(@moduleEdit.delegateEvents).toHaveBeenCalled() expect(@moduleEdit.delegateEvents).toHaveBeenCalled()
......
...@@ -8,7 +8,7 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base ...@@ -8,7 +8,7 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
<span class="published-status"> <span class="published-status">
<strong>Will Release:</strong> 06/12/2013 at 04:00 UTC <strong>Will Release:</strong> 06/12/2013 at 04:00 UTC
</span> </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> </div>
""" """
...@@ -35,8 +35,8 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base ...@@ -35,8 +35,8 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
""" """
appendSetFixtures """ appendSetFixtures """
<section class="courseware-section branch" data-id="a-location-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"> <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> <a href="#" class="delete-section-button"></a>
</li> </li>
</section> </section>
...@@ -44,19 +44,19 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base ...@@ -44,19 +44,19 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
appendSetFixtures """ appendSetFixtures """
<ol> <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"> <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-1" data-parent="subsection-1-id" data-locator="first-unit-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-2" data-parent="subsection-1-id" data-locator="second-unit-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-3" data-parent="subsection-1-id" data-locator="third-unit-id"></li>
</ol> </ol>
</li> </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"> <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> </ol>
</li> </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"> <ol class="sortable-unit-list" id="subsection-list-3">
</li> </li>
</ol> </ol>
...@@ -366,10 +366,10 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base ...@@ -366,10 +366,10 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
expect($('#unit-1')).toHaveClass('was-dropped') expect($('#unit-1')).toHaveClass('was-dropped')
# We expect 2 requests to be sent-- the first for removing Unit 1 from Subsection 1, # 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. # 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) @requests[0].respond(200)
expect(@savingSpies.hide).not.toHaveBeenCalled() 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) @requests[1].respond(200)
expect(@savingSpies.hide).toHaveBeenCalled() expect(@savingSpies.hide).toHaveBeenCalled()
# Class is removed in a timeout. # 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", ...@@ -63,20 +63,21 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata()) return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata())
createItem: (parent, payload) -> createItem: (parent, payload) ->
payload.parent_location = parent payload.parent_locator = parent
$.postJSON( $.postJSON(
"/create_item" @model.urlRoot
payload payload
(data) => (data) =>
@model.set(id: data.id) @model.set(id: data.locator)
@model.set(old_id: data.id)
@$el.data('id', data.id) @$el.data('id', data.id)
@$el.data('update_url', data.update_url) @$el.data('locator', data.locator)
@render() @render()
) )
render: -> render: ->
if @model.id if @model.get('old_id')
@$el.load("/preview_component/#{@model.id}", => @$el.load("/preview_component/#{@model.get('old_id')}", =>
@loadDisplay() @loadDisplay()
@delegateEvents() @delegateEvents()
) )
......
define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views/feedback_notification", "coffee/src/models/module", "coffee/src/views/module_edit"], define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views/feedback_notification",
($, ui, Backbone, PromptView, NotificationView, ModuleModel, ModuleEditView) -> "coffee/src/views/module_edit", "js/models/module_info", "js/utils/module"],
($, ui, Backbone, PromptView, NotificationView, ModuleEditView, ModuleModel, ModuleUtils) ->
class TabsEdit extends Backbone.View class TabsEdit extends Backbone.View
initialize: => initialize: =>
@$('.component').each((idx, element) => @$('.component').each((idx, element) =>
model = new ModuleModel({
id: $(element).data('locator'),
old_id:$(element).data('id')
})
new ModuleEditView( new ModuleEditView(
el: element, el: element,
onDelete: @deleteTab, onDelete: @deleteTab,
model: new ModuleModel( model: model
id: $(element).data('id'),
)
) )
) )
...@@ -28,7 +32,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views ...@@ -28,7 +32,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
tabMoved: (event, ui) => tabMoved: (event, ui) =>
tabs = [] tabs = []
@$('.component').each((idx, element) => @$('.component').each((idx, element) =>
tabs.push($(element).data('id')) tabs.push($(element).data('locator'))
) )
analytics.track "Reordered Static Pages", analytics.track "Reordered Static Pages",
...@@ -78,13 +82,13 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views ...@@ -78,13 +82,13 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
analytics.track "Deleted Static Page", analytics.track "Deleted Static Page",
course: course_location_analytics course: course_location_analytics
id: $component.data('id') id: $component.data('locator')
deleting = new NotificationView.Mini deleting = new NotificationView.Mini
title: gettext('Deleting&hellip;') title: gettext('Deleting&hellip;')
deleting.show() deleting.show()
$.ajax({ $.ajax({
type: 'DELETE', type: 'DELETE',
url: $component.data('update_url') url: ModuleUtils.getUpdateUrl($component.data('locator'))
}).success(=> }).success(=>
$component.remove() $component.remove()
deleting.hide() deleting.hide()
......
define ["jquery", "jquery.ui", "gettext", "backbone", define ["jquery", "jquery.ui", "gettext", "backbone",
"js/views/feedback_notification", "js/views/feedback_prompt", "js/views/feedback_notification", "js/views/feedback_prompt",
"coffee/src/models/module", "coffee/src/views/module_edit"], "coffee/src/views/module_edit", "js/models/module_info"],
($, ui, gettext, Backbone, NotificationView, PromptView, ModuleModel, ModuleEditView) -> ($, ui, gettext, Backbone, NotificationView, PromptView, ModuleEditView, ModuleModel) ->
class UnitEditView extends Backbone.View class UnitEditView extends Backbone.View
events: events:
'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates' 'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates'
...@@ -61,11 +61,13 @@ define ["jquery", "jquery.ui", "gettext", "backbone", ...@@ -61,11 +61,13 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
) )
@$('.component').each (idx, element) => @$('.component').each (idx, element) =>
model = new ModuleModel
id: $(element).data('locator')
old_id: $(element).data('id')
new ModuleEditView new ModuleEditView
el: element, el: element,
onDelete: @deleteComponent, onDelete: @deleteComponent,
model: new ModuleModel model: model
id: $(element).data('id')
showComponentTemplates: (event) => showComponentTemplates: (event) =>
event.preventDefault() event.preventDefault()
...@@ -96,7 +98,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone", ...@@ -96,7 +98,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@$newComponentItem.before(editor.$el) @$newComponentItem.before(editor.$el)
editor.createItem( editor.createItem(
@$el.data('id'), @$el.data('locator'),
$(event.currentTarget).data() $(event.currentTarget).data()
) )
...@@ -107,7 +109,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone", ...@@ -107,7 +109,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@closeNewComponent(event) @closeNewComponent(event)
components: => @$('.component').map((idx, el) -> $(el).data('id')).get() components: => @$('.component').map((idx, el) -> $(el).data('locator')).get()
wait: (value) => wait: (value) =>
@$('.unit-body').toggleClass("waiting", value) @$('.unit-body').toggleClass("waiting", value)
...@@ -136,13 +138,13 @@ define ["jquery", "jquery.ui", "gettext", "backbone", ...@@ -136,13 +138,13 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
$component = $(event.currentTarget).parents('.component') $component = $(event.currentTarget).parents('.component')
$.ajax({ $.ajax({
type: 'DELETE', type: 'DELETE',
url: $component.data('update_url') url: @model.urlRoot + "/" + $component.data('locator')
}).success(=> }).success(=>
deleting.hide() deleting.hide()
analytics.track "Deleted a Component", analytics.track "Deleted a Component",
course: course_location_analytics course: course_location_analytics
unit_id: unit_location_analytics unit_id: unit_location_analytics
id: $component.data('id') id: $component.data('locator')
$component.remove() $component.remove()
# b/c we don't vigilantly keep children up to date # b/c we don't vigilantly keep children up to date
...@@ -165,7 +167,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone", ...@@ -165,7 +167,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@wait(true) @wait(true)
$.ajax({ $.ajax({
type: 'DELETE', type: 'DELETE',
url: @$el.data('update_url') + "?" + $.param({recurse: true}) url: @model.urlRoot + "/" + @$el.data('locator') + "?" + $.param({recurse: true})
}).success(=> }).success(=>
analytics.track "Deleted Draft", analytics.track "Deleted Draft",
......
require(["domReady", "jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt", 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"], "js/utils/get_date", "js/utils/module", "jquery.ui", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"],
function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils) { function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, ModuleUtils) {
var $body; var $body;
var $newComponentItem; var $newComponentItem;
...@@ -178,7 +178,7 @@ function saveSubsection() { ...@@ -178,7 +178,7 @@ function saveSubsection() {
$spinner.show(); $spinner.show();
} }
var id = $('.subsection-body').data('id'); var locator = $('.subsection-body').data('locator');
// pull all 'normalized' metadata editable fields on page // pull all 'normalized' metadata editable fields on page
var metadata_fields = $('input[data-metadata-name]'); var metadata_fields = $('input[data-metadata-name]');
...@@ -202,12 +202,11 @@ function saveSubsection() { ...@@ -202,12 +202,11 @@ function saveSubsection() {
}); });
$.ajax({ $.ajax({
url: "/save_item", url: ModuleUtils.getUpdateUrl(locator),
type: "POST", type: "PUT",
dataType: "json", dataType: "json",
contentType: "application/json", contentType: "application/json",
data: JSON.stringify({ data: JSON.stringify({
'id': id,
'metadata': metadata 'metadata': metadata
}), }),
success: function() { success: function() {
...@@ -226,12 +225,12 @@ function createNewUnit(e) { ...@@ -226,12 +225,12 @@ function createNewUnit(e) {
analytics.track('Created a Unit', { analytics.track('Created a Unit', {
'course': course_location_analytics, 'course': course_location_analytics,
'parent_location': parent 'parent_locator': parent
}); });
$.postJSON('/create_item', { $.postJSON(ModuleUtils.getUpdateUrl(), {
'parent_location': parent, 'parent_locator': parent,
'category': category, 'category': category,
'display_name': 'New Unit' 'display_name': 'New Unit'
}, },
...@@ -267,11 +266,11 @@ function _deleteItem($el, type) { ...@@ -267,11 +266,11 @@ function _deleteItem($el, type) {
click: function(view) { click: function(view) {
view.hide(); view.hide();
var id = $el.data('id'); var locator = $el.data('locator');
analytics.track('Deleted an Item', { analytics.track('Deleted an Item', {
'course': course_location_analytics, 'course': course_location_analytics,
'id': id 'id': locator
}); });
var deleting = new NotificationView.Mini({ var deleting = new NotificationView.Mini({
...@@ -281,7 +280,7 @@ function _deleteItem($el, type) { ...@@ -281,7 +280,7 @@ function _deleteItem($el, type) {
$.ajax({ $.ajax({
type: 'DELETE', 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 () { success: function () {
$el.remove(); $el.remove();
deleting.hide(); deleting.hide();
......
define(["backbone"], function(Backbone) { define(["backbone", "js/utils/module"], function(Backbone, ModuleUtils) {
var ModuleInfo = Backbone.Model.extend({ var ModuleInfo = Backbone.Model.extend({
urlRoot: "/xblock", urlRoot: ModuleUtils.urlRoot,
defaults: { defaults: {
"id": null, "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({ var Section = Backbone.Model.extend({
defaults: { defaults: {
"name": "" "name": ""
...@@ -8,10 +10,9 @@ define(["backbone", "gettext", "js/views/feedback_notification"], function(Backb ...@@ -8,10 +10,9 @@ define(["backbone", "gettext", "js/views/feedback_notification"], function(Backb
return gettext("You must specify a name"); return gettext("You must specify a name");
} }
}, },
url: "/save_item", urlRoot: ModuleUtils.urlRoot,
toJSON: function() { toJSON: function() {
return { return {
id: this.get("id"),
metadata: { metadata: {
display_name: this.get("name") 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", define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification", "draggabilly",
"js/utils/modal", "js/utils/cancel_on_escape", "js/utils/get_date"], "js/utils/modal", "js/utils/cancel_on_escape", "js/utils/get_date", "js/utils/module"],
function (domReady, $, ui, _, gettext, NotificationView, Draggabilly, ModalUtils, CancelOnEscape, DateUtils) { function (domReady, $, ui, _, gettext, NotificationView, Draggabilly, ModalUtils, CancelOnEscape,
DateUtils, ModuleUtils) {
var modalSelector = '.edit-subsection-publish-settings'; var modalSelector = '.edit-subsection-publish-settings';
...@@ -37,7 +38,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe ...@@ -37,7 +38,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var editSectionPublishDate = function (e) { var editSectionPublishDate = function (e) {
e.preventDefault(); e.preventDefault();
var $modal = $(modalSelector); 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-date').val($(this).attr('data-date'));
$modal.find('.start-time').val($(this).attr('data-time')); $modal.find('.start-time').val($(this).attr('data-time'));
if ($modal.find('.start-date').val() == '' && $modal.find('.start-time').val() == '') { 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 ...@@ -55,11 +56,11 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
$('.edit-subsection-publish-settings .start-time') $('.edit-subsection-publish-settings .start-time')
); );
var id = $(modalSelector).attr('data-id'); var locator = $(modalSelector).attr('data-locator');
analytics.track('Edited Section Release Date', { analytics.track('Edited Section Release Date', {
'course': course_location_analytics, 'course': course_location_analytics,
'id': id, 'id': locator,
'start': datetime 'start': datetime
}); });
...@@ -69,12 +70,11 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe ...@@ -69,12 +70,11 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
saving.show(); saving.show();
// call into server to commit the new order // call into server to commit the new order
$.ajax({ $.ajax({
url: "/save_item", url: ModuleUtils.getUpdateUrl(locator),
type: "POST", type: "PUT",
dataType: "json", dataType: "json",
contentType: "application/json", contentType: "application/json",
data: JSON.stringify({ data: JSON.stringify({
'id': id,
'metadata': { 'metadata': {
'start': datetime 'start': datetime
} }
...@@ -86,18 +86,18 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe ...@@ -86,18 +86,18 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
return (number < 10 ? '0' : '') + number; return (number < 10 ? '0' : '') + number;
}; };
var $thisSection = $('.courseware-section[data-id="' + id + '"]'); var $thisSection = $('.courseware-section[data-locator="' + locator + '"]');
var html = _.template( var html = _.template(
'<span class="published-status">' + '<span class="published-status">' +
'<strong>' + gettext("Will Release:") + '&nbsp;</strong>' + '<strong>' + gettext("Will Release:") + '&nbsp;</strong>' +
gettext("{month}/{day}/{year} at {hour}:{minute} UTC") + gettext("{month}/{day}/{year} at {hour}:{minute} UTC") +
'</span>' + '</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") + gettext("Edit") +
'</a>', '</a>',
{year: datetime.getUTCFullYear(), month: pad2(datetime.getUTCMonth() + 1), day: pad2(datetime.getUTCDate()), {year: datetime.getUTCFullYear(), month: pad2(datetime.getUTCMonth() + 1), day: pad2(datetime.getUTCDate()),
hour: pad2(datetime.getUTCHours()), minute: pad2(datetime.getUTCMinutes()), hour: pad2(datetime.getUTCHours()), minute: pad2(datetime.getUTCMinutes()),
id: id}, locator: locator},
{interpolate: /\{(.+?)\}/g}); {interpolate: /\{(.+?)\}/g});
$thisSection.find('.section-published-date').html(html); $thisSection.find('.section-published-date').html(html);
ModalUtils.hideModal(); ModalUtils.hideModal();
...@@ -132,14 +132,14 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe ...@@ -132,14 +132,14 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
'display_name': display_name 'display_name': display_name
}); });
$.postJSON('/create_item', { $.postJSON(ModuleUtils.getUpdateUrl(), {
'parent_location': parent, 'parent_locator': parent,
'category': category, 'category': category,
'display_name': display_name 'display_name': display_name
}, },
function(data) { 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 ...@@ -159,7 +159,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var $saveButton = $newSubsection.find('.new-subsection-name-save'); var $saveButton = $newSubsection.find('.new-subsection-name-save');
var $cancelButton = $newSubsection.find('.new-subsection-name-cancel'); 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('parent', parent);
$saveButton.data('category', $(this).data('category')); $saveButton.data('category', $(this).data('category'));
...@@ -182,14 +182,14 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe ...@@ -182,14 +182,14 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
}); });
$.postJSON('/create_item', { $.postJSON(ModuleUtils.getUpdateUrl(), {
'parent_location': parent, 'parent_locator': parent,
'category': category, 'category': category,
'display_name': display_name 'display_name': display_name
}, },
function(data) { function(data) {
if (data.id != undefined) { if (data.locator != undefined) {
location.reload(); location.reload();
} }
}); });
...@@ -219,7 +219,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe ...@@ -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 // Exclude the 'new unit' buttons, and make sure we don't
// prepend an element to itself // prepend an element to itself
var siblings = container.children().filter(function () { 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 // If the container is collapsed, check to see if the
// element is on top of its parent list -- don't check 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 ...@@ -416,16 +416,16 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var parentSelector = ele.data('parent-location-selector'); var parentSelector = ele.data('parent-location-selector');
var childrenSelector = ele.data('child-selector'); var childrenSelector = ele.data('child-selector');
var newParentEle = ele.parents(parentSelector).first(); var newParentEle = ele.parents(parentSelector).first();
var newParentID = newParentEle.data('id'); var newParentLocator = newParentEle.data('locator');
var oldParentID = ele.data('parent-id'); var oldParentLocator = ele.data('parent');
// If the parent has changed, update the children of the old parent. // If the parent has changed, update the children of the old parent.
if (oldParentID !== newParentID) { if (newParentLocator !== oldParentLocator) {
// Find the old parent element. // Find the old parent element.
var oldParentEle = $(parentSelector).filter(function () { var oldParentEle = $(parentSelector).filter(function () {
return $(this).data('id') === oldParentID; return $(this).data('locator') === oldParentLocator;
}); });
this.saveItem(oldParentEle, childrenSelector, function () { this.saveItem(oldParentEle, childrenSelector, function () {
ele.data('parent-id', newParentID); ele.data('parent', newParentLocator);
}); });
} }
var saving = new NotificationView.Mini({ var saving = new NotificationView.Mini({
...@@ -452,16 +452,15 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe ...@@ -452,16 +452,15 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var children = _.map( var children = _.map(
ele.find(childrenSelector), ele.find(childrenSelector),
function (child) { function (child) {
return $(child).data('id'); return $(child).data('locator');
} }
); );
$.ajax({ $.ajax({
url: '/save_item', url: ModuleUtils.getUpdateUrl(ele.data('locator')),
type: 'POST', type: 'PUT',
dataType: 'json', dataType: 'json',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify({ data: JSON.stringify({
id: ele.data('id'),
children: children children: children
}), }),
success: success success: success
......
...@@ -68,7 +68,7 @@ src_paths: ...@@ -68,7 +68,7 @@ src_paths:
spec_paths: spec_paths:
- coffee/spec/main.js - coffee/spec/main.js
- coffee/spec - coffee/spec
- js_spec - js/spec
# Paths to fixture files (optional) # Paths to fixture files (optional)
# The fixture path will be set automatically when using jasmine-jquery. # The fixture path will be set automatically when using jasmine-jquery.
......
...@@ -9,11 +9,11 @@ ...@@ -9,11 +9,11 @@
<%block name="jsextra"> <%block name="jsextra">
<script type='text/javascript'> <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({ new TabsEditView({
el: $('.main-wrapper'), el: $('.main-wrapper'),
model: new ModuleModel({ model: new Backbone.Model({
id: '${context_course.location}' id: '${locator}'
}), }),
mast: $('.wrapper-mast') mast: $('.wrapper-mast')
}); });
...@@ -61,8 +61,8 @@ require(["coffee/src/views/tabs", "coffee/src/models/module"], function(TabsEdit ...@@ -61,8 +61,8 @@ require(["coffee/src/views/tabs", "coffee/src/models/module"], function(TabsEdit
<div class="tab-list"> <div class="tab-list">
<ol class='components'> <ol class='components'>
% for id, update_url in components: % for id, locator in components:
<li class="component" data-id="${id}" data-update_url="${update_url}"/> <li class="component" data-id="${id}" data-locator="${locator}"/>
% endfor % endfor
<li class="new-component-item"> <li class="new-component-item">
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-wrapper"> <div class="inner-wrapper">
<div class="main-column"> <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"> <div class="subsection-name-input">
<label>${_("Display Name:")}</label> <label>${_("Display Name:")}</label>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/> <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 ...@@ -39,7 +39,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
$(".section-name").each(function() { $(".section-name").each(function() {
var model = new SectionModel({ var model = new SectionModel({
id: $(this).parent(".item-details").data("id"), id: $(this).parent(".item-details").data("locator"),
name: $(this).data("name") name: $(this).data("name")
}); });
new SectionShowView({model: model, el: this}).render(); new SectionShowView({model: model, el: this}).render();
...@@ -57,7 +57,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -57,7 +57,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<h3 class="section-name"> <h3 class="section-name">
<form class="section-name-form"> <form class="section-name-form">
<input type="text" value="${_('New Section Name')}" class="new-section-name" /> <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')}" /> data-category="${new_section_category}" value="${_('Save')}" />
<input type="button" class="new-section-name-cancel" value="${_('Cancel')}" /></h3> <input type="button" class="new-section-name-cancel" value="${_('Cancel')}" /></h3>
</form> </form>
...@@ -75,7 +75,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -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> <span class="section-name-span">Click here to set the section name</span>
<form class="section-name-form"> <form class="section-name-form">
<input type="text" value="${_('New Section Name')}" class="new-section-name" /> <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')}" /> data-category="${new_section_category}" value="${_('Save')}" />
<input type="button" class="new-section-name-cancel" value="$(_('Cancel')}" /></h3> <input type="button" class="new-section-name-cancel" value="$(_('Cancel')}" /></h3>
</form> </form>
...@@ -140,22 +140,26 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -140,22 +140,26 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<div class="inner-wrapper"> <div class="inner-wrapper">
<div class="wrapper-dnd"> <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: % 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 context_course.location.course_id, section.location, False, True
).url_reverse('xblock') )
%> %>
<section class="courseware-section branch is-draggable" data-id="${section.location}" <section class="courseware-section branch is-draggable" data-parent="${course_locator}"
data-parent-id="${context_course.location.url()}" data-update_url="${section_update_url}"> data-locator="${section_locator}">
<%include file="widgets/_ui-dnd-indicator-before.html" /> <%include file="widgets/_ui-dnd-indicator-before.html" />
<header> <header>
<a href="#" data-tooltip="${_('Expand/collapse this section')}" class="expand-collapse-icon collapse"></a> <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> <h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
<div class="section-published-date"> <div class="section-published-date">
<% <%
...@@ -168,12 +172,12 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -168,12 +172,12 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
%> %>
%if section.start is None: %if section.start is None:
<span class="published-status">${_("This section has not been released.")}</span> <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: %else:
<span class="published-status"><strong>${_("Will Release:")}</strong> <span class="published-status"><strong>${_("Will Release:")}</strong>
${date_utils.get_default_time_display(section.start)}</span> ${date_utils.get_default_time_display(section.start)}</span>
<a href="#" class="edit-button" data-date="${start_date_str}" <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 %endif
</div> </div>
</div> </div>
...@@ -189,15 +193,15 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -189,15 +193,15 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<span class="new-folder-icon"></span>${_("New Subsection")} <span class="new-folder-icon"></span>${_("New Subsection")}
</a> </a>
</div> </div>
<ol class="sortable-subsection-list" data-id="${section.location.url()}"> <ol class="sortable-subsection-list">
% for subsection in section.get_children(): % 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 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}" <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" /> <%include file="widgets/_ui-dnd-indicator-before.html" />
......
...@@ -24,7 +24,6 @@ CMS.URL.LMS_BASE = "${settings.LMS_BASE}" ...@@ -24,7 +24,6 @@ CMS.URL.LMS_BASE = "${settings.LMS_BASE}"
require(["js/models/section", "js/collections/textbook", "js/views/list_textbooks"], require(["js/models/section", "js/collections/textbook", "js/views/list_textbooks"],
function(Section, TextbookCollection, ListTextbooksView) { function(Section, TextbookCollection, ListTextbooksView) {
window.section = new Section({ window.section = new Section({
id: "${course.id}",
name: "${course.display_name_with_default | h}", name: "${course.display_name_with_default | h}",
url_name: "${course.location.name | h}", url_name: "${course.location.name | h}",
org: "${course.location.org | h}", org: "${course.location.org | h}",
......
...@@ -10,9 +10,9 @@ from xmodule.modulestore.django import loc_mapper ...@@ -10,9 +10,9 @@ from xmodule.modulestore.django import loc_mapper
<%block name="jsextra"> <%block name="jsextra">
<script type='text/javascript'> <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) { function(doc, $, ModuleModel, UnitEditView, ui) {
window.unit_location_analytics = '${unit_location}'; window.unit_location_analytics = '${unit_locator}';
// tabs // tabs
$('.tab-group').tabs(); $('.tab-group').tabs();
...@@ -20,7 +20,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un ...@@ -20,7 +20,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
new UnitEditView({ new UnitEditView({
el: $('.main-wrapper'), el: $('.main-wrapper'),
model: new ModuleModel({ model: new ModuleModel({
id: '${unit_location}', id: '${unit_locator}',
state: '${unit_state}' state: '${unit_state}'
}) })
}); });
...@@ -34,7 +34,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un ...@@ -34,7 +34,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
</%block> </%block>
<%block name="content"> <%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="inner-wrapper">
<div class="alert editing-draft-alert"> <div class="alert editing-draft-alert">
<p class="alert-message"><strong>${_("You are editing a draft.")}</strong> <p class="alert-message"><strong>${_("You are editing a draft.")}</strong>
...@@ -48,8 +48,8 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un ...@@ -48,8 +48,8 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
<article class="unit-body window"> <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> <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"> <ol class="components">
% for id, component_update_url in components: % for id, locator in components:
<li class="component" data-id="${id}" data-update_url="${component_update_url}"/> <li class="component" data-id="${id}" data-locator="${locator}"/>
% endfor % endfor
<li class="new-component-item adding"> <li class="new-component-item adding">
<div class="new-component"> <div class="new-component">
...@@ -141,7 +141,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un ...@@ -141,7 +141,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
<div class="window-contents"> <div class="window-contents">
<div class="row visibility"> <div class="row visibility">
<label class="inline-label">${_("Visibility:")}</label> <label class="inline-label">${_("Visibility:")}</label>
<select class='visibility-select'> <select name="visibility-select" class='visibility-select'>
<option value="public">${_("Public")}</option> <option value="public">${_("Public")}</option>
<option value="private">${_("Private")}</option> <option value="private">${_("Private")}</option>
</select> </select>
......
...@@ -11,12 +11,15 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -11,12 +11,15 @@ This def will enumerate through a passed in subsection and list all of the units
if subsection_units is None: if subsection_units is None:
subsection_units = subsection.get_children() 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: % 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()}" <li class="courseware-unit leaf unit is-draggable" data-locator="${unit_locator}"
data-update_url="${unit_update_url}" > data-parent="${subsection_locator}">
<%include file="_ui-dnd-indicator-before.html" /> <%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 ...@@ -34,8 +37,7 @@ This def will enumerate through a passed in subsection and list all of the units
</a> </a>
% if actions: % if actions:
<div class="item-actions"> <div class="item-actions">
<a href="#" data-tooltip="Delete this unit" class="delete-button" data-id="${unit.location}" <a href="#" data-tooltip="Delete this unit" class="delete-button" data-locator="${unit_locator}">
data-update_url="${unit_update_url}">
<span class="delete-icon"></span></a> <span class="delete-icon"></span></a>
<span data-tooltip="Drag to sort" class="drag-handle unit-drag-handle"></span> <span data-tooltip="Drag to sort" class="drag-handle unit-drag-handle"></span>
</div> </div>
...@@ -48,7 +50,7 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -48,7 +50,7 @@ This def will enumerate through a passed in subsection and list all of the units
<li> <li>
<%include file="_ui-dnd-indicator-initial.html" /> <%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 <span class="new-unit-icon"></span>New Unit
</a> </a>
</li> </li>
......
...@@ -15,8 +15,6 @@ urlpatterns = patterns('', # nopep8 ...@@ -15,8 +15,6 @@ urlpatterns = patterns('', # nopep8
url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'), 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'^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'^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/upload$', 'contentstore.views.upload_transcripts', name='upload_transcripts'),
url(r'^transcripts/download$', 'contentstore.views.download_transcripts', name='download_transcripts'), url(r'^transcripts/download$', 'contentstore.views.download_transcripts', name='download_transcripts'),
...@@ -110,12 +108,12 @@ urlpatterns += patterns( ...@@ -110,12 +108,12 @@ urlpatterns += patterns(
), ),
url(r'(?ix)^course($|/){}$'.format(parsers.URL_RE_SOURCE), 'course_handler'), url(r'(?ix)^course($|/){}$'.format(parsers.URL_RE_SOURCE), 'course_handler'),
url(r'(?ix)^checklists/{}(/)?(?P<checklist_index>\d+)?$'.format(parsers.URL_RE_SOURCE), 'checklists_handler'), url(r'(?ix)^checklists/{}(/)?(?P<checklist_index>\d+)?$'.format(parsers.URL_RE_SOURCE), 'checklists_handler'),
url(r'(?ix)^orphan/{}$'.format(parsers.URL_RE_SOURCE), 'orphan'), url(r'(?ix)^orphan/{}$'.format(parsers.URL_RE_SOURCE), 'orphan_handler'),
url(r'(?ix)^assets/{}(/)?(?P<asset_id>.+)?$'.format(parsers.URL_RE_SOURCE), 'assets_handler'), url(r'(?ix)^assets/{}(/)?(?P<asset_id>.+)?$'.format(parsers.URL_RE_SOURCE), 'assets_handler'),
url(r'(?ix)^import/{}$'.format(parsers.URL_RE_SOURCE), 'import_handler'), 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)^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)^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 = { js_info_dict = {
......
...@@ -31,7 +31,7 @@ REQUIREJS_WAIT = { ...@@ -31,7 +31,7 @@ REQUIREJS_WAIT = {
# Individual Unit (editing) # Individual Unit (editing)
re.compile('^Individual Unit \|'): [ 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"], "coffee/src/views/module_edit"],
# Content - Outline # Content - Outline
......
"""
Utilities for string manipulation.
"""
def str_to_bool(str):
"""
Converts "true" (case-insensitive) to the boolean True.
Everything else will return False (including None).
An error will be thrown for non-string input (besides None).
"""
return False if str is None else str.lower() == "true"
"""
Tests for string_utils.py
"""
from django.test import TestCase
from util.string_utils import str_to_bool
class StringUtilsTest(TestCase):
"""
Tests for str_to_bool.
"""
def test_str_to_bool_true(self):
self.assertTrue(str_to_bool('True'))
self.assertTrue(str_to_bool('true'))
self.assertTrue(str_to_bool('trUe'))
def test_str_to_bool_false(self):
self.assertFalse(str_to_bool('Tru'))
self.assertFalse(str_to_bool('False'))
self.assertFalse(str_to_bool('false'))
self.assertFalse(str_to_bool(''))
self.assertFalse(str_to_bool(None))
self.assertFalse(str_to_bool('anything'))
def test_str_to_bool_errors(self):
def test_raises_error(val):
with self.assertRaises(AttributeError):
self.assertFalse(str_to_bool(val))
test_raises_error({})
test_raises_error([])
test_raises_error(1)
test_raises_error(True)
...@@ -184,12 +184,17 @@ class DraftModuleStore(MongoModuleStore): ...@@ -184,12 +184,17 @@ class DraftModuleStore(MongoModuleStore):
location: Something that can be passed to Location location: Something that can be passed to Location
children: A list of child item identifiers children: A list of child item identifiers
""" """
# We expect the children IDs to always be the non-draft version. With view refactoring
# for split, we are now passing the draft version in some cases.
children_ids = [as_published(child).url() for child in children]
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) draft_item = self.get_item(location)
if not getattr(draft_item, 'is_draft', False): if not getattr(draft_item, 'is_draft', False):
self.convert_to_draft(as_published(location)) 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): 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