Commit 1b2be30c by cahrens

Convert tabs to RESTful URL.

STUD-850
parent 82092bdd
......@@ -9,10 +9,8 @@ Feature: CMS.Static Pages
Then I should see a static page named "Empty"
Scenario: Users can delete static pages
Given I have opened a new course in Studio
And I go to the static pages page
And I add a new page
And I "delete" the static page
Given I have created a static page
When I "delete" the static page
Then I am shown a prompt
When I confirm the prompt
Then I should not see any static pages
......@@ -20,9 +18,16 @@ Feature: CMS.Static Pages
# Safari won't update the name properly
@skip_safari
Scenario: Users can edit static pages
Given I have opened a new course in Studio
And I go to the static pages page
And I add a new page
Given I have created a static page
When I "edit" the static page
And I change the name to "New"
Then I should see a static page named "New"
# Safari won't update the name properly
@skip_safari
Scenario: Users can reorder static pages
Given I have created two different static pages
When I reorder the tabs
Then the tabs are in the reverse order
And I reload the page
Then the tabs are in the reverse order
......@@ -48,3 +48,47 @@ def change_name(step, new_name):
world.trigger_event(input_css)
save_button = 'a.save-button'
world.css_click(save_button)
@step(u'I reorder the tabs')
def reorder_tabs(_step):
# For some reason, the drag_and_drop method did not work in this case.
draggables = world.css_find('.drag-handle')
source = draggables.first
target = draggables.last
source.action_chains.click_and_hold(source._element).perform()
source.action_chains.move_to_element_with_offset(target._element, 0, 50).perform()
source.action_chains.release().perform()
@step(u'I have created a static page')
def create_static_page(step):
step.given('I have opened a new course in Studio')
step.given('I go to the static pages page')
step.given('I add a new page')
@step(u'I have created two different static pages')
def create_two_pages(step):
step.given('I have created a static page')
step.given('I "edit" the static page')
step.given('I change the name to "First"')
step.given('I add a new page')
# Verify order of tabs
_verify_tab_names('First', 'Empty')
@step(u'the tabs are in the reverse order')
def tabs_in_reverse_order(step):
_verify_tab_names('Empty', 'First')
def _verify_tab_names(first, second):
world.wait_for(
func=lambda _: len(world.css_find('.xmodule_StaticTabModule')) == 2,
timeout=200,
timeout_msg="Timed out waiting for two tabs to be present"
)
tabs = world.css_find('.xmodule_StaticTabModule')
assert tabs[0].text == first
assert tabs[1].text == second
......@@ -179,7 +179,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# TODO: uncomment after edit_unit not using locations.
# _test_no_locations(self, resp)
def lockAnAsset(self, content_store, course_location):
def _lock_an_asset(self, content_store, course_location):
"""
Lock an arbitrary asset in the course
:param course_location:
......@@ -407,24 +407,7 @@ 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')
locator = _course_factory_create_course()
course_location = loc_mapper().translate_locator_to_location(locator)
ItemFactory.create(
parent_location=course_location,
category="static_tab",
display_name="Static_1")
ItemFactory.create(
parent_location=course_location,
category="static_tab",
display_name="Static_2")
module_store, course_location, new_location = self._create_static_tabs()
course = module_store.get_item(course_location)
......@@ -432,9 +415,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
reverse_tabs = []
for tab in course.tabs:
if tab['type'] == 'static_tab':
reverse_tabs.insert(0, get_tab_locator(tab))
reverse_tabs.insert(0, unicode(self._get_tab_locator(course, tab)))
self.client.ajax_post(reverse('reorder_static_tabs'), {'tabs': reverse_tabs})
self.client.ajax_post(new_location.url_reverse('tabs'), {'tabs': reverse_tabs})
course = module_store.get_item(course_location)
......@@ -442,10 +425,57 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
course_tabs = []
for tab in course.tabs:
if tab['type'] == 'static_tab':
course_tabs.append(get_tab_locator(tab))
course_tabs.append(unicode(self._get_tab_locator(course, tab)))
self.assertEqual(reverse_tabs, course_tabs)
def test_static_tab_deletion(self):
module_store, course_location, _ = self._create_static_tabs()
course = module_store.get_item(course_location)
num_tabs = len(course.tabs)
last_tab = course.tabs[num_tabs - 1]
url_slug = last_tab['url_slug']
delete_url = self._get_tab_locator(course, last_tab).url_reverse('xblock')
self.client.delete(delete_url)
course = module_store.get_item(course_location)
self.assertEqual(num_tabs - 1, len(course.tabs))
def tab_matches(tab):
""" Checks if the tab matches the one we deleted """
return tab['type'] == 'static_tab' and tab['url_slug'] == url_slug
tab_found = any(tab_matches(tab) for tab in course.tabs)
self.assertFalse(tab_found, "tab should have been deleted")
def _get_tab_locator(self, course, tab):
""" Returns the locator for a given tab. """
tab_location = 'i4x://MITx/999/static_tab/{0}'.format(tab['url_slug'])
return loc_mapper().translate_location(
course.location.course_id, Location(tab_location), False, True
)
def _create_static_tabs(self):
""" Creates two static tabs in a dummy course. """
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])
new_location = loc_mapper().translate_location(course_location.course_id, course_location, False, True)
ItemFactory.create(
parent_location=course_location,
category="static_tab",
display_name="Static_1")
ItemFactory.create(
parent_location=course_location,
category="static_tab",
display_name="Static_2")
return module_store, course_location, new_location
def test_import_polls(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['toy'])
......@@ -633,7 +663,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
thumbnail = content_store.find(thumbnail_location, throw_on_not_found=False)
self.assertIsNotNone(thumbnail)
def _delete_asset_in_course (self):
def _delete_asset_in_course(self):
"""
Helper method for:
1) importing course from xml
......@@ -972,7 +1002,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertIn(private_location_no_draft.url(), sequential.children)
locked_asset = self.lockAnAsset(content_store, location)
locked_asset = self._lock_an_asset(content_store, location)
locked_asset_attrs = content_store.get_attrs(locked_asset)
# the later import will reupload
del locked_asset_attrs['uploadDate']
......@@ -1027,7 +1057,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
shutil.rmtree(root_dir)
def check_import(self, module_store, root_dir, draft_store, content_store, stub_location, course_location,
locked_asset, locked_asset_attrs):
locked_asset, locked_asset_attrs):
# reimport
import_from_xml(
module_store, root_dir, ['test_export'], draft_store=draft_store,
......@@ -1226,7 +1256,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
handouts_locator = loc_mapper().translate_location('edX/toy/2012_Fall', handout_location)
# get module info (json)
resp = self.client.get(handouts_locator.url_reverse('/xblock', ''))
resp = self.client.get(handouts_locator.url_reverse('/xblock'))
# make sure we got a successful response
self.assertEqual(resp.status_code, 200)
......@@ -1403,7 +1433,7 @@ class ContentStoreTest(ModuleStoreTestCase):
second_course_data = self.assert_created_course(number_suffix=uuid4().hex)
# unseed the forums for the first course
course_id =_get_course_id(test_course_data)
course_id = _get_course_id(test_course_data)
delete_course_and_groups(course_id, commit=True)
self.assertFalse(are_permissions_roles_seeded(course_id))
......@@ -1625,6 +1655,7 @@ class ContentStoreTest(ModuleStoreTestCase):
test_get_html('course_info')
test_get_html('checklists')
test_get_html('assets')
test_get_html('tabs')
# settings_details
resp = self.client.get_html(reverse('settings_details',
......@@ -1677,13 +1708,6 @@ class ContentStoreTest(ModuleStoreTestCase):
# TODO: uncomment when edit_unit not using old locations.
# _test_no_locations(self, resp)
resp = self.client.get_html(reverse('edit_tabs',
kwargs={'org': loc.org,
'course': loc.course,
'coursename': loc.name}))
self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
def delete_item(category, name):
""" Helper method for testing the deletion of an xblock item. """
del_loc = loc.replace(category=category, name=name)
......@@ -1997,7 +2021,7 @@ def _create_course(test, course_data):
test.assertEqual(response.status_code, 200)
data = parse_json(response)
test.assertNotIn('ErrMsg', data)
test.assertEqual(data['url'], new_location.url_reverse("course/", ""))
test.assertEqual(data['url'], new_location.url_reverse("course"))
def _course_factory_create_course():
......
......@@ -197,7 +197,8 @@ define([
"js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec",
"js/spec/transcripts/file_uploader_spec",
"js/spec/utils/module_spec"
"js/spec/utils/module_spec",
"js/spec/models/explicit_url_spec"
# these tests are run separate in the cms-squire suite, due to process
# isolation issues with Squire.js
......
......@@ -37,14 +37,17 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
analytics.track "Reordered Static Pages",
course: course_location_analytics
saving = new NotificationView.Mini({title: gettext("Saving…")})
saving.show()
$.ajax({
type:'POST',
url: '/reorder_static_tabs',
url: @model.url(),
data: JSON.stringify({
tabs : tabs
}),
contentType: 'application/json'
})
}).success(=> saving.hide())
addNewTab: (event) =>
event.preventDefault()
......
/**
* A model that simply allows the update URL to be passed
* in as an argument.
*/
define(["backbone"], function(Backbone){
return Backbone.Model.extend({
defaults: {
"explicit_url": ""
},
url: function() {
return this.get("explicit_url");
}
});
});
define(['js/models/explicit_url'],
function (Model) {
describe('Model ', function () {
it('allows url to be passed in constructor', function () {
expect(new Model({'explicit_url': '/fancy/url'}).url()).toBe('/fancy/url');
});
it('returns empty string if url not set', function () {
expect(new Model().url()).toBe('');
});
});
}
);
......@@ -9,12 +9,15 @@
<%block name="jsextra">
<script type='text/javascript'>
require(["backbone", "coffee/src/views/tabs"], function(Backbone, TabsEditView) {
require(["js/models/explicit_url", "coffee/src/views/tabs"], function(TabsModel, TabsEditView) {
var model = new TabsModel({
id: "${course_locator}",
explicit_url: "${course_locator.url_reverse('tabs')}"
});
new TabsEditView({
el: $('.main-wrapper'),
model: new Backbone.Model({
id: '${locator}'
}),
model: model,
mast: $('.wrapper-mast')
});
});
......
......@@ -16,13 +16,14 @@
<%
ctx_loc = context_course.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
index_url = location.url_reverse('course/')
checklists_url = location.url_reverse('checklists/')
course_team_url = location.url_reverse('course_team/')
assets_url = location.url_reverse('assets/')
import_url = location.url_reverse('import/')
course_info_url = location.url_reverse('course_info/')
export_url = location.url_reverse('export/', '')
index_url = location.url_reverse('course')
checklists_url = location.url_reverse('checklists')
course_team_url = location.url_reverse('course_team')
assets_url = location.url_reverse('assets')
import_url = location.url_reverse('import')
course_info_url = location.url_reverse('course_info')
export_url = location.url_reverse('export')
tabs_url = location.url_reverse('tabs')
%>
<h2 class="info-course">
<span class="sr">${_("Current Course:")}</span>
......@@ -48,7 +49,7 @@
<a href="${course_info_url}">${_("Updates")}</a>
</li>
<li class="nav-item nav-course-courseware-pages">
<a href="${reverse('edit_tabs', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, coursename=ctx_loc.name))}">${_("Static Pages")}</a>
<a href="${tabs_url}">${_("Static Pages")}</a>
</li>
<li class="nav-item nav-course-courseware-uploads">
<a href="${assets_url}">${_("Files &amp; Uploads")}</a>
......
......@@ -26,7 +26,6 @@ urlpatterns = patterns('', # nopep8
url(r'^create_draft$', 'contentstore.views.create_draft', name='create_draft'),
url(r'^publish_draft$', 'contentstore.views.publish_draft', name='publish_draft'),
url(r'^unpublish_unit$', 'contentstore.views.unpublish_unit', name='unpublish_unit'),
url(r'^reorder_static_tabs', 'contentstore.views.reorder_static_tabs', name='reorder_static_tabs'),
url(r'^preview/xblock/(?P<usage_id>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>[^/]*))?$',
'contentstore.views.preview_handler', name='preview_handler'),
......@@ -49,9 +48,6 @@ urlpatterns = patterns('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$',
'contentstore.views.assignment_type_update', name='assignment_type_update'),
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
'contentstore.views.edit_tabs', name='edit_tabs'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$',
'contentstore.views.textbook_index', name='textbook_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/new$',
......@@ -113,6 +109,7 @@ urlpatterns += patterns(
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)^tabs/{}$'.format(parsers.URL_RE_SOURCE), 'tabs_handler'),
)
js_info_dict = {
......
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