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,
in roughly chronological order, most recent first. Add your entries at or near
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
text like with bold or italics. (BLD-449)
......
......@@ -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)
......@@ -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',
......
......@@ -13,8 +13,9 @@ 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
from ..utils import get_modulestore
from django.utils.translation import ugettext as _
......@@ -47,12 +48,18 @@ 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_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()
course = get_modulestore(course_location).get_item(course_location)
# 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
# that we know about) otherwise we can drop some!
......@@ -64,7 +71,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 +129,20 @@ def edit_tabs(request, org, course, coursename):
static_tab.location.url(),
loc_mapper().translate_location(
course_item.location.course_id, static_tab.location, False, True
).url_reverse("xblock")
)
]
for static_tab
in static_tabs
]
course_locator = loc_mapper().translate_location(
course_item.location.course_id, course_item.location, False, True
)
return render_to_response('edit-tabs.html', {
'context_course': course_item,
'components': components
'components': components,
'locator': course_locator
})
......
......@@ -182,7 +182,7 @@ define([
"coffee/spec/main_spec",
"coffee/spec/models/course_spec", "coffee/spec/models/metadata_spec",
"coffee/spec/models/module_spec", "coffee/spec/models/section_spec",
"coffee/spec/models/section_spec",
"coffee/spec/models/settings_course_grader_spec",
"coffee/spec/models/settings_grading_spec", "coffee/spec/models/textbook_spec",
"coffee/spec/models/upload_spec",
......@@ -193,9 +193,11 @@ define([
"coffee/spec/views/overview_spec",
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
"js_spec/transcripts/utils_spec", "js_spec/transcripts/editor_spec",
"js_spec/transcripts/videolist_spec", "js_spec/transcripts/message_manager_spec",
"js_spec/transcripts/file_uploader_spec"
"js/spec/transcripts/utils_spec", "js/spec/transcripts/editor_spec",
"js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec",
"js/spec/transcripts/file_uploader_spec",
"js/spec/utils/module_spec"
# these tests are run separate in the cms-squire suite, due to process
# isolation issues with Squire.js
......
define ["coffee/src/models/module"], (Module) ->
describe "Module", ->
it "set the correct URL", ->
expect(new Module().url).toEqual("/save_item")
it "set the correct default", ->
expect(new Module().defaults).toEqual(undefined)
define ["js/models/section", "sinon"], (Section, sinon) ->
define ["js/models/section", "sinon", "js/utils/module"], (Section, sinon, ModuleUtils) ->
describe "Section", ->
describe "basic", ->
beforeEach ->
@model = new Section({
id: 42,
id: 42
name: "Life, the Universe, and Everything"
})
......@@ -14,11 +14,10 @@ define ["js/models/section", "sinon"], (Section, sinon) ->
expect(@model.get("name")).toEqual("Life, the Universe, and Everything")
it "should have a URL set", ->
expect(@model.url).toEqual("/save_item")
expect(@model.url()).toEqual(ModuleUtils.getUpdateUrl(42))
it "should serialize to JSON correctly", ->
expect(@model.toJSON()).toEqual({
id: 42,
metadata:
{
display_name: "Life, the Universe, and Everything"
......@@ -30,7 +29,7 @@ define ["js/models/section", "sinon"], (Section, sinon) ->
spyOn(Section.prototype, 'showNotification')
spyOn(Section.prototype, 'hideNotification')
@model = new Section({
id: 42,
id: 42
name: "Life, the Universe, and Everything"
})
@requests = requests = []
......
......@@ -4,6 +4,9 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
beforeEach ->
@stubModule = jasmine.createSpy("Module")
@stubModule.id = 'stub-id'
@stubModule.get = (param)->
if param == 'old_id'
return 'stub-old-id'
setFixtures """
<li class="component" id="stub-id">
......@@ -59,7 +62,7 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
@moduleEdit.render()
it "loads the module preview and editor via ajax on the view element", ->
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/preview_component/#{@moduleEdit.model.id}", jasmine.any(Function))
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/preview_component/#{@moduleEdit.model.get('old_id')}", jasmine.any(Function))
@moduleEdit.$el.load.mostRecentCall.args[1]()
expect(@moduleEdit.loadDisplay).toHaveBeenCalled()
expect(@moduleEdit.delegateEvents).toHaveBeenCalled()
......
......@@ -8,7 +8,7 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
<span class="published-status">
<strong>Will Release:</strong> 06/12/2013 at 04:00 UTC
</span>
<a href="#" class="edit-button" data-date="06/12/2013" data-time="04:00" data-id="i4x://pfogg/42/chapter/d6b47f7b084f49debcaf67fe5436c8e2">Edit</a>
<a href="#" class="edit-button" data-date="06/12/2013" data-time="04:00" data-locator="i4x://pfogg/42/chapter/d6b47f7b084f49debcaf67fe5436c8e2">Edit</a>
</div>
"""
......@@ -35,8 +35,8 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
"""
appendSetFixtures """
<section class="courseware-section branch" data-id="a-location-goes-here">
<li class="branch collapsed id-holder" data-id="an-id-goes-here">
<section class="courseware-section branch" data-locator="a-location-goes-here">
<li class="branch collapsed id-holder" data-id="an-id-goes-here" data-locator="an-id-goes-here">
<a href="#" class="delete-section-button"></a>
</li>
</section>
......@@ -44,19 +44,19 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
appendSetFixtures """
<ol>
<li class="subsection-list branch" data-id="subsection-1-id" id="subsection-1">
<li class="subsection-list branch" id="subsection-1" data-locator="subsection-1-id">
<ol class="sortable-unit-list" id="subsection-list-1">
<li class="unit" id="unit-1" data-id="first-unit-id" data-parent-id="subsection-1-id"></li>
<li class="unit" id="unit-2" data-id="second-unit-id" data-parent-id="subsection-1-id"></li>
<li class="unit" id="unit-3" data-id="third-unit-id" data-parent-id="subsection-1-id"></li>
<li class="unit" id="unit-1" data-parent="subsection-1-id" data-locator="first-unit-id"></li>
<li class="unit" id="unit-2" data-parent="subsection-1-id" data-locator="second-unit-id"></li>
<li class="unit" id="unit-3" data-parent="subsection-1-id" data-locator="third-unit-id"></li>
</ol>
</li>
<li class="subsection-list branch" data-id="subsection-2-id" id="subsection-2">
<li class="subsection-list branch" id="subsection-2" data-locator="subsection-2-id">
<ol class="sortable-unit-list" id="subsection-list-2">
<li class="unit" id="unit-4" data-id="fourth-unit-id" data-parent-id="subsection-2"></li>
<li class="unit" id="unit-4" data-parent="subsection-2" data-locator="fourth-unit-id"></li>
</ol>
</li>
<li class="subsection-list branch" data-id="subsection-3-id" id="subsection-3">
<li class="subsection-list branch" id="subsection-3" data-locator="subsection-3-id">
<ol class="sortable-unit-list" id="subsection-list-3">
</li>
</ol>
......@@ -366,10 +366,10 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
expect($('#unit-1')).toHaveClass('was-dropped')
# We expect 2 requests to be sent-- the first for removing Unit 1 from Subsection 1,
# and the second for adding Unit 1 to the end of Subsection 2.
expect(@requests[0].requestBody).toEqual('{"id":"subsection-1-id","children":["second-unit-id","third-unit-id"]}')
expect(@requests[0].requestBody).toEqual('{"children":["second-unit-id","third-unit-id"]}')
@requests[0].respond(200)
expect(@savingSpies.hide).not.toHaveBeenCalled()
expect(@requests[1].requestBody).toEqual('{"id":"subsection-2-id","children":["fourth-unit-id","first-unit-id"]}')
expect(@requests[1].requestBody).toEqual('{"children":["fourth-unit-id","first-unit-id"]}')
@requests[1].respond(200)
expect(@savingSpies.hide).toHaveBeenCalled()
# Class is removed in a timeout.
......
define ["backbone"], (Backbone) ->
class Module extends Backbone.Model
url: '/save_item'
......@@ -63,20 +63,21 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata())
createItem: (parent, payload) ->
payload.parent_location = parent
payload.parent_locator = parent
$.postJSON(
"/create_item"
@model.urlRoot
payload
(data) =>
@model.set(id: data.id)
@model.set(id: data.locator)
@model.set(old_id: data.id)
@$el.data('id', data.id)
@$el.data('update_url', data.update_url)
@$el.data('locator', data.locator)
@render()
)
render: ->
if @model.id
@$el.load("/preview_component/#{@model.id}", =>
if @model.get('old_id')
@$el.load("/preview_component/#{@model.get('old_id')}", =>
@loadDisplay()
@delegateEvents()
)
......
define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views/feedback_notification", "coffee/src/models/module", "coffee/src/views/module_edit"],
($, ui, Backbone, PromptView, NotificationView, ModuleModel, ModuleEditView) ->
define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views/feedback_notification",
"coffee/src/views/module_edit", "js/models/module_info", "js/utils/module"],
($, ui, Backbone, PromptView, NotificationView, ModuleEditView, ModuleModel, ModuleUtils) ->
class TabsEdit extends Backbone.View
initialize: =>
@$('.component').each((idx, element) =>
model = new ModuleModel({
id: $(element).data('locator'),
old_id:$(element).data('id')
})
new ModuleEditView(
el: element,
onDelete: @deleteTab,
model: new ModuleModel(
id: $(element).data('id'),
)
model: model
)
)
......@@ -28,7 +32,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
tabMoved: (event, ui) =>
tabs = []
@$('.component').each((idx, element) =>
tabs.push($(element).data('id'))
tabs.push($(element).data('locator'))
)
analytics.track "Reordered Static Pages",
......@@ -78,13 +82,13 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
analytics.track "Deleted Static Page",
course: course_location_analytics
id: $component.data('id')
id: $component.data('locator')
deleting = new NotificationView.Mini
title: gettext('Deleting&hellip;')
deleting.show()
$.ajax({
type: 'DELETE',
url: $component.data('update_url')
url: ModuleUtils.getUpdateUrl($component.data('locator'))
}).success(=>
$component.remove()
deleting.hide()
......
define ["jquery", "jquery.ui", "gettext", "backbone",
"js/views/feedback_notification", "js/views/feedback_prompt",
"coffee/src/models/module", "coffee/src/views/module_edit"],
($, ui, gettext, Backbone, NotificationView, PromptView, ModuleModel, ModuleEditView) ->
"coffee/src/views/module_edit", "js/models/module_info"],
($, ui, gettext, Backbone, NotificationView, PromptView, ModuleEditView, ModuleModel) ->
class UnitEditView extends Backbone.View
events:
'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates'
......@@ -61,11 +61,13 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
)
@$('.component').each (idx, element) =>
model = new ModuleModel
id: $(element).data('locator')
old_id: $(element).data('id')
new ModuleEditView
el: element,
onDelete: @deleteComponent,
model: new ModuleModel
id: $(element).data('id')
model: model
showComponentTemplates: (event) =>
event.preventDefault()
......@@ -96,7 +98,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@$newComponentItem.before(editor.$el)
editor.createItem(
@$el.data('id'),
@$el.data('locator'),
$(event.currentTarget).data()
)
......@@ -107,7 +109,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@closeNewComponent(event)
components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
components: => @$('.component').map((idx, el) -> $(el).data('locator')).get()
wait: (value) =>
@$('.unit-body').toggleClass("waiting", value)
......@@ -136,13 +138,13 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
$component = $(event.currentTarget).parents('.component')
$.ajax({
type: 'DELETE',
url: $component.data('update_url')
url: @model.urlRoot + "/" + $component.data('locator')
}).success(=>
deleting.hide()
analytics.track "Deleted a Component",
course: course_location_analytics
unit_id: unit_location_analytics
id: $component.data('id')
id: $component.data('locator')
$component.remove()
# b/c we don't vigilantly keep children up to date
......@@ -165,7 +167,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@wait(true)
$.ajax({
type: 'DELETE',
url: @$el.data('update_url') + "?" + $.param({recurse: true})
url: @model.urlRoot + "/" + @$el.data('locator') + "?" + $.param({recurse: true})
}).success(=>
analytics.track "Deleted Draft",
......
require(["domReady", "jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt",
"js/utils/get_date", "jquery.ui", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"],
function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils) {
"js/utils/get_date", "js/utils/module", "jquery.ui", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"],
function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, ModuleUtils) {
var $body;
var $newComponentItem;
......@@ -178,7 +178,7 @@ function saveSubsection() {
$spinner.show();
}
var id = $('.subsection-body').data('id');
var locator = $('.subsection-body').data('locator');
// pull all 'normalized' metadata editable fields on page
var metadata_fields = $('input[data-metadata-name]');
......@@ -202,12 +202,11 @@ function saveSubsection() {
});
$.ajax({
url: "/save_item",
type: "POST",
url: ModuleUtils.getUpdateUrl(locator),
type: "PUT",
dataType: "json",
contentType: "application/json",
data: JSON.stringify({
'id': id,
'metadata': metadata
}),
success: function() {
......@@ -226,12 +225,12 @@ function createNewUnit(e) {
analytics.track('Created a Unit', {
'course': course_location_analytics,
'parent_location': parent
'parent_locator': parent
});
$.postJSON('/create_item', {
'parent_location': parent,
$.postJSON(ModuleUtils.getUpdateUrl(), {
'parent_locator': parent,
'category': category,
'display_name': 'New Unit'
},
......@@ -267,11 +266,11 @@ function _deleteItem($el, type) {
click: function(view) {
view.hide();
var id = $el.data('id');
var locator = $el.data('locator');
analytics.track('Deleted an Item', {
'course': course_location_analytics,
'id': id
'id': locator
});
var deleting = new NotificationView.Mini({
......@@ -281,7 +280,7 @@ function _deleteItem($el, type) {
$.ajax({
type: 'DELETE',
url: $el.data('update_url')+'?'+ $.param({recurse: true, all_versions: true}),
url: ModuleUtils.getUpdateUrl(locator) +'?'+ $.param({recurse: true, all_versions: true}),
success: function () {
$el.remove();
deleting.hide();
......
define(["backbone"], function(Backbone) {
define(["backbone", "js/utils/module"], function(Backbone, ModuleUtils) {
var ModuleInfo = Backbone.Model.extend({
urlRoot: "/xblock",
urlRoot: ModuleUtils.urlRoot,
defaults: {
"id": null,
......
define(["backbone", "gettext", "js/views/feedback_notification"], function(Backbone, gettext, NotificationView) {
define(["backbone", "gettext", "js/views/feedback_notification", "js/utils/module"],
function(Backbone, gettext, NotificationView, ModuleUtils) {
var Section = Backbone.Model.extend({
defaults: {
"name": ""
......@@ -8,10 +10,9 @@ define(["backbone", "gettext", "js/views/feedback_notification"], function(Backb
return gettext("You must specify a name");
}
},
url: "/save_item",
urlRoot: ModuleUtils.urlRoot,
toJSON: function() {
return {
id: this.get("id"),
metadata: {
display_name: this.get("name")
}
......
define(['js/utils/module'],
function (ModuleUtils) {
describe('urlRoot ', function () {
it('defines xblock urlRoot', function () {
expect(ModuleUtils.urlRoot).toBe('/xblock');
});
});
describe('getUpdateUrl ', function () {
it('can take no arguments', function () {
expect(ModuleUtils.getUpdateUrl()).toBe('/xblock');
});
it('appends a locator', function () {
expect(ModuleUtils.getUpdateUrl("locator")).toBe('/xblock/locator');
});
});
}
);
/**
* Utilities for modules/xblocks.
*
* Returns:
*
* urlRoot: the root for creating/updating an xblock.
* getUpdateUrl: a utility method that returns the xblock update URL, appending
* the location if passed in.
*/
define([], function () {
var urlRoot = '/xblock';
var getUpdateUrl = function (locator) {
if (locator === undefined) {
return urlRoot;
}
else {
return urlRoot + "/" + locator;
}
};
return {
urlRoot: urlRoot,
getUpdateUrl: getUpdateUrl
};
});
define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification", "draggabilly",
"js/utils/modal", "js/utils/cancel_on_escape", "js/utils/get_date"],
function (domReady, $, ui, _, gettext, NotificationView, Draggabilly, ModalUtils, CancelOnEscape, DateUtils) {
"js/utils/modal", "js/utils/cancel_on_escape", "js/utils/get_date", "js/utils/module"],
function (domReady, $, ui, _, gettext, NotificationView, Draggabilly, ModalUtils, CancelOnEscape,
DateUtils, ModuleUtils) {
var modalSelector = '.edit-subsection-publish-settings';
......@@ -37,7 +38,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var editSectionPublishDate = function (e) {
e.preventDefault();
var $modal = $(modalSelector);
$modal.attr('data-id', $(this).attr('data-id'));
$modal.attr('data-locator', $(this).attr('data-locator'));
$modal.find('.start-date').val($(this).attr('data-date'));
$modal.find('.start-time').val($(this).attr('data-time'));
if ($modal.find('.start-date').val() == '' && $modal.find('.start-time').val() == '') {
......@@ -55,11 +56,11 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
$('.edit-subsection-publish-settings .start-time')
);
var id = $(modalSelector).attr('data-id');
var locator = $(modalSelector).attr('data-locator');
analytics.track('Edited Section Release Date', {
'course': course_location_analytics,
'id': id,
'id': locator,
'start': datetime
});
......@@ -69,12 +70,11 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
saving.show();
// call into server to commit the new order
$.ajax({
url: "/save_item",
type: "POST",
url: ModuleUtils.getUpdateUrl(locator),
type: "PUT",
dataType: "json",
contentType: "application/json",
data: JSON.stringify({
'id': id,
'metadata': {
'start': datetime
}
......@@ -86,18 +86,18 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
return (number < 10 ? '0' : '') + number;
};
var $thisSection = $('.courseware-section[data-id="' + id + '"]');
var $thisSection = $('.courseware-section[data-locator="' + locator + '"]');
var html = _.template(
'<span class="published-status">' +
'<strong>' + gettext("Will Release:") + '&nbsp;</strong>' +
gettext("{month}/{day}/{year} at {hour}:{minute} UTC") +
'</span>' +
'<a href="#" class="edit-button" data-date="{month}/{day}/{year}" data-time="{hour}:{minute}" data-id="{id}">' +
'<a href="#" class="edit-button" data-date="{month}/{day}/{year}" data-time="{hour}:{minute}" data-locator="{locator}">' +
gettext("Edit") +
'</a>',
{year: datetime.getUTCFullYear(), month: pad2(datetime.getUTCMonth() + 1), day: pad2(datetime.getUTCDate()),
hour: pad2(datetime.getUTCHours()), minute: pad2(datetime.getUTCMinutes()),
id: id},
locator: locator},
{interpolate: /\{(.+?)\}/g});
$thisSection.find('.section-published-date').html(html);
ModalUtils.hideModal();
......@@ -132,14 +132,14 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
'display_name': display_name
});
$.postJSON('/create_item', {
'parent_location': parent,
$.postJSON(ModuleUtils.getUpdateUrl(), {
'parent_locator': parent,
'category': category,
'display_name': display_name
},
function(data) {
if (data.id != undefined) location.reload();
if (data.locator != undefined) location.reload();
});
};
......@@ -159,7 +159,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var $saveButton = $newSubsection.find('.new-subsection-name-save');
var $cancelButton = $newSubsection.find('.new-subsection-name-cancel');
var parent = $(this).parents("section.branch").data("id");
var parent = $(this).parents("section.branch").data("locator");
$saveButton.data('parent', parent);
$saveButton.data('category', $(this).data('category'));
......@@ -182,14 +182,14 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
});
$.postJSON('/create_item', {
'parent_location': parent,
$.postJSON(ModuleUtils.getUpdateUrl(), {
'parent_locator': parent,
'category': category,
'display_name': display_name
},
function(data) {
if (data.id != undefined) {
if (data.locator != undefined) {
location.reload();
}
});
......@@ -219,7 +219,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
// Exclude the 'new unit' buttons, and make sure we don't
// prepend an element to itself
var siblings = container.children().filter(function () {
return $(this).data('id') !== undefined && !$(this).is(ele);
return $(this).data('locator') !== undefined && !$(this).is(ele);
});
// If the container is collapsed, check to see if the
// element is on top of its parent list -- don't check the
......@@ -416,16 +416,16 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var parentSelector = ele.data('parent-location-selector');
var childrenSelector = ele.data('child-selector');
var newParentEle = ele.parents(parentSelector).first();
var newParentID = newParentEle.data('id');
var oldParentID = ele.data('parent-id');
var newParentLocator = newParentEle.data('locator');
var oldParentLocator = ele.data('parent');
// If the parent has changed, update the children of the old parent.
if (oldParentID !== newParentID) {
if (newParentLocator !== oldParentLocator) {
// Find the old parent element.
var oldParentEle = $(parentSelector).filter(function () {
return $(this).data('id') === oldParentID;
return $(this).data('locator') === oldParentLocator;
});
this.saveItem(oldParentEle, childrenSelector, function () {
ele.data('parent-id', newParentID);
ele.data('parent', newParentLocator);
});
}
var saving = new NotificationView.Mini({
......@@ -452,16 +452,15 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
var children = _.map(
ele.find(childrenSelector),
function (child) {
return $(child).data('id');
return $(child).data('locator');
}
);
$.ajax({
url: '/save_item',
type: 'POST',
url: ModuleUtils.getUpdateUrl(ele.data('locator')),
type: 'PUT',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({
id: ele.data('id'),
children: children
}),
success: success
......
......@@ -68,7 +68,7 @@ src_paths:
spec_paths:
- coffee/spec/main.js
- coffee/spec
- js_spec
- js/spec
# Paths to fixture files (optional)
# The fixture path will be set automatically when using jasmine-jquery.
......
......@@ -9,11 +9,11 @@
<%block name="jsextra">
<script type='text/javascript'>
require(["coffee/src/views/tabs", "coffee/src/models/module"], function(TabsEditView, ModuleModel) {
require(["backbone", "coffee/src/views/tabs"], function(Backbone, TabsEditView) {
new TabsEditView({
el: $('.main-wrapper'),
model: new ModuleModel({
id: '${context_course.location}'
model: new Backbone.Model({
id: '${locator}'
}),
mast: $('.wrapper-mast')
});
......@@ -61,8 +61,8 @@ require(["coffee/src/views/tabs", "coffee/src/models/module"], function(TabsEdit
<div class="tab-list">
<ol class='components'>
% for id, update_url in components:
<li class="component" data-id="${id}" data-update_url="${update_url}"/>
% for id, locator in components:
<li class="component" data-id="${id}" data-locator="${locator}"/>
% endfor
<li class="new-component-item">
......
......@@ -16,7 +16,7 @@
<div class="main-wrapper">
<div class="inner-wrapper">
<div class="main-column">
<article class="subsection-body window" data-id="${subsection.location}">
<article class="subsection-body window" data-locator="${locator}">
<div class="subsection-name-input">
<label>${_("Display Name:")}</label>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
......
......@@ -39,7 +39,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
$(".section-name").each(function() {
var model = new SectionModel({
id: $(this).parent(".item-details").data("id"),
id: $(this).parent(".item-details").data("locator"),
name: $(this).data("name")
});
new SectionShowView({model: model, el: this}).render();
......@@ -57,7 +57,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<h3 class="section-name">
<form class="section-name-form">
<input type="text" value="${_('New Section Name')}" class="new-section-name" />
<input type="submit" class="new-section-name-save" data-parent="${parent_location}"
<input type="submit" class="new-section-name-save" data-parent="${parent_locator}"
data-category="${new_section_category}" value="${_('Save')}" />
<input type="button" class="new-section-name-cancel" value="${_('Cancel')}" /></h3>
</form>
......@@ -75,7 +75,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<span class="section-name-span">Click here to set the section name</span>
<form class="section-name-form">
<input type="text" value="${_('New Section Name')}" class="new-section-name" />
<input type="submit" class="new-section-name-save" data-parent="${parent_location}"
<input type="submit" class="new-section-name-save" data-parent="${parent_locator}"
data-category="${new_section_category}" value="${_('Save')}" />
<input type="button" class="new-section-name-cancel" value="$(_('Cancel')}" /></h3>
</form>
......@@ -140,22 +140,26 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<div class="inner-wrapper">
<div class="wrapper-dnd">
<article class="courseware-overview" data-id="${context_course.location.url()}">
<%
course_locator = loc_mapper().translate_location(
context_course.location.course_id, context_course.location, False, True
)
%>
<article class="courseware-overview" data-locator="${course_locator}">
% for section in sections:
<%
section_update_url = loc_mapper().translate_location(
section_locator = loc_mapper().translate_location(
context_course.location.course_id, section.location, False, True
).url_reverse('xblock')
)
%>
<section class="courseware-section branch is-draggable" data-id="${section.location}"
data-parent-id="${context_course.location.url()}" data-update_url="${section_update_url}">
<section class="courseware-section branch is-draggable" data-parent="${course_locator}"
data-locator="${section_locator}">
<%include file="widgets/_ui-dnd-indicator-before.html" />
<header>
<a href="#" data-tooltip="${_('Expand/collapse this section')}" class="expand-collapse-icon collapse"></a>
<div class="item-details" data-id="${section.location}">
<div class="item-details" data-locator="${section_locator}">
<h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
<div class="section-published-date">
<%
......@@ -168,12 +172,12 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
%>
%if section.start is None:
<span class="published-status">${_("This section has not been released.")}</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">${_("Schedule")}</a>
<a href="#" class="schedule-button" data-date="" data-time="" data-locator="${section_locator}">${_("Schedule")}</a>
%else:
<span class="published-status"><strong>${_("Will Release:")}</strong>
${date_utils.get_default_time_display(section.start)}</span>
<a href="#" class="edit-button" data-date="${start_date_str}"
data-time="${start_time_str}" data-id="${section.location}">${_("Edit")}</a>
data-time="${start_time_str}" data-locator="${section_locator}">${_("Edit")}</a>
%endif
</div>
</div>
......@@ -189,15 +193,15 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<span class="new-folder-icon"></span>${_("New Subsection")}
</a>
</div>
<ol class="sortable-subsection-list" data-id="${section.location.url()}">
<ol class="sortable-subsection-list">
% for subsection in section.get_children():
<%
subsection_update_url = loc_mapper().translate_location(
subsection_locator = loc_mapper().translate_location(
context_course.location.course_id, subsection.location, False, True
).url_reverse('xblock')
)
%>
<li class="courseware-subsection branch collapsed id-holder is-draggable" data-id="${subsection.location}"
data-parent-id="${section.location.url()}" data-update_url="${subsection_update_url}">
data-parent="${section_locator}" data-locator="${subsection_locator}">
<%include file="widgets/_ui-dnd-indicator-before.html" />
......
......@@ -24,7 +24,6 @@ CMS.URL.LMS_BASE = "${settings.LMS_BASE}"
require(["js/models/section", "js/collections/textbook", "js/views/list_textbooks"],
function(Section, TextbookCollection, ListTextbooksView) {
window.section = new Section({
id: "${course.id}",
name: "${course.display_name_with_default | h}",
url_name: "${course.location.name | h}",
org: "${course.location.org | h}",
......
......@@ -10,9 +10,9 @@ from xmodule.modulestore.django import loc_mapper
<%block name="jsextra">
<script type='text/javascript'>
require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/unit", "jquery.ui"],
require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit", "jquery.ui"],
function(doc, $, ModuleModel, UnitEditView, ui) {
window.unit_location_analytics = '${unit_location}';
window.unit_location_analytics = '${unit_locator}';
// tabs
$('.tab-group').tabs();
......@@ -20,7 +20,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
new UnitEditView({
el: $('.main-wrapper'),
model: new ModuleModel({
id: '${unit_location}',
id: '${unit_locator}',
state: '${unit_state}'
})
});
......@@ -34,7 +34,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
</%block>
<%block name="content">
<div class="main-wrapper edit-state-${unit_state}" data-id="${unit_location}" data-update_url="${unit_update_url}">
<div class="main-wrapper edit-state-${unit_state}" data-id="${unit_location}" data-locator="${unit_locator}">
<div class="inner-wrapper">
<div class="alert editing-draft-alert">
<p class="alert-message"><strong>${_("You are editing a draft.")}</strong>
......@@ -48,8 +48,8 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
<article class="unit-body window">
<p class="unit-name-input"><label>${_("Display Name:")}</label><input type="text" value="${unit.display_name_with_default | h}" class="unit-display-name-input" /></p>
<ol class="components">
% for id, component_update_url in components:
<li class="component" data-id="${id}" data-update_url="${component_update_url}"/>
% for id, locator in components:
<li class="component" data-id="${id}" data-locator="${locator}"/>
% endfor
<li class="new-component-item adding">
<div class="new-component">
......@@ -141,7 +141,7 @@ require(["domReady!", "jquery", "coffee/src/models/module", "coffee/src/views/un
<div class="window-contents">
<div class="row visibility">
<label class="inline-label">${_("Visibility:")}</label>
<select class='visibility-select'>
<select name="visibility-select" class='visibility-select'>
<option value="public">${_("Public")}</option>
<option value="private">${_("Private")}</option>
</select>
......
......@@ -11,12 +11,15 @@ This def will enumerate through a passed in subsection and list all of the units
if subsection_units is None:
subsection_units = subsection.get_children()
%>
<%
subsection_locator = loc_mapper().translate_location(context_course.location.course_id, subsection.location, False, True)
%>
% for unit in subsection_units:
<%
unit_update_url = loc_mapper().translate_location(context_course.location.course_id, unit.location, False, True).url_reverse('xblock')
unit_locator = loc_mapper().translate_location(context_course.location.course_id, unit.location, False, True)
%>
<li class="courseware-unit leaf unit is-draggable" data-id="${unit.location}" data-parent-id="${subsection.location.url()}"
data-update_url="${unit_update_url}" >
<li class="courseware-unit leaf unit is-draggable" data-locator="${unit_locator}"
data-parent="${subsection_locator}">
<%include file="_ui-dnd-indicator-before.html" />
......@@ -34,8 +37,7 @@ This def will enumerate through a passed in subsection and list all of the units
</a>
% if actions:
<div class="item-actions">
<a href="#" data-tooltip="Delete this unit" class="delete-button" data-id="${unit.location}"
data-update_url="${unit_update_url}">
<a href="#" data-tooltip="Delete this unit" class="delete-button" data-locator="${unit_locator}">
<span class="delete-icon"></span></a>
<span data-tooltip="Drag to sort" class="drag-handle unit-drag-handle"></span>
</div>
......@@ -48,7 +50,7 @@ This def will enumerate through a passed in subsection and list all of the units
<li>
<%include file="_ui-dnd-indicator-initial.html" />
<a href="#" class="new-unit-item" data-category="${new_unit_category}" data-parent="${subsection.location}">
<a href="#" class="new-unit-item" data-category="${new_unit_category}" data-parent="${subsection_locator}">
<span class="new-unit-icon"></span>New Unit
</a>
</li>
......
......@@ -15,8 +15,6 @@ urlpatterns = patterns('', # nopep8
url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'),
url(r'^subsection/(?P<location>.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'),
url(r'^preview_component/(?P<location>.*?)$', 'contentstore.views.preview_component', name='preview_component'),
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
url(r'^create_item$', 'contentstore.views.create_item', name='create_item'),
url(r'^transcripts/upload$', 'contentstore.views.upload_transcripts', name='upload_transcripts'),
url(r'^transcripts/download$', 'contentstore.views.download_transcripts', name='download_transcripts'),
......@@ -110,12 +108,12 @@ urlpatterns += patterns(
),
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)^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)^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
......
"""
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):
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 = [as_published(child).url() for child in children]
draft_loc = as_draft(location)
draft_item = self.get_item(location)
if not getattr(draft_item, 'is_draft', False):
self.convert_to_draft(as_published(location))
return super(DraftModuleStore, self).update_children(draft_loc, children)
return super(DraftModuleStore, self).update_children(draft_loc, children_ids)
def update_metadata(self, location, metadata):
"""
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment