diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 3ec9b1429795..cc1c04c55e01 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -205,7 +205,7 @@ def xblock_view_handler(request, usage_key_string, view_name):
if 'application/json' in accept_header:
store = modulestore()
xblock = store.get_item(usage_key)
- container_views = ['container_preview', 'reorderable_container_child_preview']
+ container_views = ['container_preview', 'reorderable_container_child_preview', 'container_child_preview']
# wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly
@@ -237,12 +237,32 @@ def xblock_view_handler(request, usage_key_string, view_name):
if view_name == 'reorderable_container_child_preview':
reorderable_items.add(xblock.location)
+ paging = None
+ try:
+ if request.REQUEST.get('enable_paging', 'false') == 'true':
+ paging = {
+ 'page_number': int(request.REQUEST.get('page_number', 0)),
+ 'page_size': int(request.REQUEST.get('page_size', 0)),
+ }
+ except ValueError:
+ return HttpResponse(
+ content="Couldn't parse paging parameters: enable_paging: "
+ "%s, page_number: %s, page_size: %s".format(
+ request.REQUEST.get('enable_paging', 'false'),
+ request.REQUEST.get('page_number', 0),
+ request.REQUEST.get('page_size', 0)
+ ),
+ status=400,
+ content_type="text/plain",
+ )
+
# Set up the context to be passed to each XBlock's render method.
context = {
'is_pages_view': is_pages_view, # This setting disables the recursive wrapping of xblocks
'is_unit_page': is_unit(xblock),
'root_xblock': xblock if (view_name == 'container_preview') else None,
- 'reorderable_items': reorderable_items
+ 'reorderable_items': reorderable_items,
+ 'paging': paging,
}
fragment = get_preview_fragment(request, xblock, context)
diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee
index 3a7b2c046a32..b83442a9a630 100644
--- a/cms/static/coffee/spec/main.coffee
+++ b/cms/static/coffee/spec/main.coffee
@@ -239,6 +239,7 @@ define([
"js/spec/views/assets_spec",
"js/spec/views/baseview_spec",
"js/spec/views/container_spec",
+ "js/spec/views/paged_container_spec",
"js/spec/views/group_configuration_spec",
"js/spec/views/paging_spec",
"js/spec/views/unit_outline_spec",
diff --git a/cms/static/js/factories/container.js b/cms/static/js/factories/container.js
index 93cdeb8fd991..429ae58f5151 100644
--- a/cms/static/js/factories/container.js
+++ b/cms/static/js/factories/container.js
@@ -1,22 +1,20 @@
define([
- 'jquery', 'js/models/xblock_info', 'js/views/pages/container',
+ 'jquery', 'underscore', 'js/models/xblock_info', 'js/views/pages/container',
'js/collections/component_template', 'xmodule', 'coffee/src/main',
'xblock/cms.runtime.v1'
],
-function($, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
+function($, _, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
'use strict';
- return function (componentTemplates, XBlockInfoJson, action, isUnitPage) {
- var templates = new ComponentTemplates(componentTemplates, {parse: true}),
- mainXBlockInfo = new XBlockInfo(XBlockInfoJson, {parse: true});
+ return function (componentTemplates, XBlockInfoJson, action, options) {
+ var main_options = {
+ el: $('#content'),
+ model: new XBlockInfo(XBlockInfoJson, {parse: true}),
+ action: action,
+ templates: new ComponentTemplates(componentTemplates, {parse: true})
+ };
xmoduleLoader.done(function () {
- var view = new ContainerPage({
- el: $('#content'),
- model: mainXBlockInfo,
- action: action,
- templates: templates,
- isUnitPage: isUnitPage
- });
+ var view = new ContainerPage(_.extend(main_options, options));
view.render();
});
};
diff --git a/cms/static/js/factories/library.js b/cms/static/js/factories/library.js
index 2729a3cf279d..76ac47413ddc 100644
--- a/cms/static/js/factories/library.js
+++ b/cms/static/js/factories/library.js
@@ -1,22 +1,21 @@
define([
- 'jquery', 'js/models/xblock_info', 'js/views/pages/container',
- 'js/collections/component_template', 'xmodule', 'coffee/src/main',
+ 'jquery', 'underscore', 'js/models/xblock_info', 'js/views/pages/paged_container',
+ 'js/views/library_container', 'js/collections/component_template', 'xmodule', 'coffee/src/main',
'xblock/cms.runtime.v1'
],
-function($, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
+function($, _, XBlockInfo, PagedContainerPage, LibraryContainerView, ComponentTemplates, xmoduleLoader) {
'use strict';
- return function (componentTemplates, XBlockInfoJson) {
- var templates = new ComponentTemplates(componentTemplates, {parse: true}),
- mainXBlockInfo = new XBlockInfo(XBlockInfoJson, {parse: true});
+ return function (componentTemplates, XBlockInfoJson, options) {
+ var main_options = {
+ el: $('#content'),
+ model: new XBlockInfo(XBlockInfoJson, {parse: true}),
+ templates: new ComponentTemplates(componentTemplates, {parse: true}),
+ action: 'view',
+ viewClass: LibraryContainerView
+ };
xmoduleLoader.done(function () {
- var view = new ContainerPage({
- el: $('#content'),
- model: mainXBlockInfo,
- action: "view",
- templates: templates,
- isUnitPage: false
- });
+ var view = new PagedContainerPage(_.extend(main_options, options));
view.render();
});
};
diff --git a/cms/static/js/spec/views/paged_container_spec.js b/cms/static/js/spec/views/paged_container_spec.js
new file mode 100644
index 000000000000..524f88e552f7
--- /dev/null
+++ b/cms/static/js/spec/views/paged_container_spec.js
@@ -0,0 +1,489 @@
+define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/models/xblock_info",
+ "js/views/paged_container", "js/views/paging_header", "js/views/paging_footer"],
+ function ($, _, AjaxHelpers, URI, XBlockInfo, PagedContainer, PagingHeader, PagingFooter) {
+
+ var htmlResponseTpl = _.template('' +
+ '
'
+ );
+
+ function getResponseHtml(options){
+ return '' +
+ '' +
+ htmlResponseTpl(options) +
+ '' +
+ '
'
+ }
+
+ var PAGE_SIZE = 3;
+
+ var mockFirstPage = {
+ resources: [],
+ html: getResponseHtml({
+ start: 0,
+ displayed: PAGE_SIZE,
+ total: PAGE_SIZE + 1
+ })
+ };
+
+ var mockSecondPage = {
+ resources: [],
+ html: getResponseHtml({
+ start: PAGE_SIZE,
+ displayed: 1,
+ total: PAGE_SIZE + 1
+ })
+ };
+
+ var mockEmptyPage = {
+ resources: [],
+ html: getResponseHtml({
+ start: 0,
+ displayed: 0,
+ total: 0
+ })
+ };
+
+ var respondWithMockPage = function(requests) {
+ var requestIndex = requests.length - 1;
+ var request = requests[requestIndex];
+ var url = new URI(request.url);
+ var queryParameters = url.query(true); // Returns an object with each query parameter stored as a value
+ var page = queryParameters.page_number;
+ var response = page === "0" ? mockFirstPage : mockSecondPage;
+ AjaxHelpers.respondWithJson(requests, response, requestIndex);
+ };
+
+ var MockPagingView = PagedContainer.extend({
+ view: 'container_preview',
+ el: $(""),
+ model: new XBlockInfo({}, {parse: true})
+ });
+
+ describe("Paging Container", function() {
+ var pagingContainer;
+
+ beforeEach(function () {
+ var feedbackTpl = readFixtures('system-feedback.underscore');
+ setFixtures($("
+
+
+
+
+
+
+
diff --git a/cms/templates/js/mock/mock-xblock-paged.underscore b/cms/templates/js/mock/mock-xblock-paged.underscore
new file mode 100644
index 000000000000..c6c2c881d8d2
--- /dev/null
+++ b/cms/templates/js/mock/mock-xblock-paged.underscore
@@ -0,0 +1,21 @@
+
diff --git a/cms/templates/library.html b/cms/templates/library.html
index 70bd836baddd..d367c333d270 100644
--- a/cms/templates/library.html
+++ b/cms/templates/library.html
@@ -22,8 +22,11 @@
<%block name="requirejs">
require(["js/factories/library"], function(LibraryFactory) {
LibraryFactory(
- ${component_templates | n},
- ${json.dumps(xblock_info) | n}
+ ${component_templates | n}, ${json.dumps(xblock_info) | n},
+ {
+ isUnitPage: false,
+ page_size: 10
+ }
);
});
%block>
diff --git a/common/lib/xmodule/xmodule/library_root_xblock.py b/common/lib/xmodule/xmodule/library_root_xblock.py
index dc00aaa97fa9..6a58cf1b1d18 100644
--- a/common/lib/xmodule/xmodule/library_root_xblock.py
+++ b/common/lib/xmodule/xmodule/library_root_xblock.py
@@ -3,10 +3,10 @@
"""
import logging
-from .studio_editable import StudioEditableModule
from xblock.core import XBlock
from xblock.fields import Scope, String, List
from xblock.fragment import Fragment
+from xmodule.studio_editable import StudioEditableModule
log = logging.getLogger(__name__)
@@ -42,29 +42,53 @@ def __str__(self):
def author_view(self, context):
"""
- Renders the Studio preview view, which supports drag and drop.
+ Renders the Studio preview view.
"""
fragment = Fragment()
+ self.render_children(context, fragment, can_reorder=False, can_add=True)
+ return fragment
+
+ def render_children(self, context, fragment, can_reorder=False, can_add=False): # pylint: disable=unused-argument
+ """
+ Renders the children of the module with HTML appropriate for Studio. Reordering is not supported.
+ """
contents = []
- for child_key in self.children: # pylint: disable=E1101
- context['reorderable_items'].add(child_key)
+ paging = context.get('paging', None)
+
+ children_count = len(self.children) # pylint: disable=no-member
+ item_start, item_end = 0, children_count
+
+ # TODO sort children
+ if paging:
+ page_number = paging.get('page_number', 0)
+ raw_page_size = paging.get('page_size', None)
+ page_size = raw_page_size if raw_page_size is not None else children_count
+ item_start, item_end = page_size * page_number, page_size * (page_number + 1)
+
+ children_to_show = self.children[item_start:item_end] # pylint: disable=no-member
+
+ for child_key in children_to_show: # pylint: disable=E1101
child = self.runtime.get_block(child_key)
- rendered_child = self.runtime.render_child(child, StudioEditableModule.get_preview_view_name(child), context)
+ child_view_name = StudioEditableModule.get_preview_view_name(child)
+ rendered_child = self.runtime.render_child(child, child_view_name, context)
fragment.add_frag_resources(rendered_child)
contents.append({
- 'id': unicode(child_key),
+ 'id': unicode(child.location),
'content': rendered_child.content,
})
- fragment.add_content(self.runtime.render_template("studio_render_children_view.html", {
- 'items': contents,
- 'xblock_context': context,
- 'can_add': True,
- 'can_reorder': True,
- }))
- return fragment
+ fragment.add_content(
+ self.runtime.render_template("studio_render_paged_children_view.html", {
+ 'items': contents,
+ 'xblock_context': context,
+ 'can_add': can_add,
+ 'first_displayed': item_start,
+ 'total_children': children_count,
+ 'displayed_children': len(children_to_show)
+ })
+ )
@property
def display_org_with_default(self):
diff --git a/common/test/acceptance/pages/studio/library.py b/common/test/acceptance/pages/studio/library.py
index e87c556da968..64f93f21167e 100644
--- a/common/test/acceptance/pages/studio/library.py
+++ b/common/test/acceptance/pages/studio/library.py
@@ -3,13 +3,14 @@
"""
from bok_choy.page_object import PageObject
+from ...pages.studio.pagination import PaginatedMixin
from .container import XBlockWrapper
from ...tests.helpers import disable_animations
from .utils import confirm_prompt, wait_for_notification
from . import BASE_URL
-class LibraryPage(PageObject):
+class LibraryPage(PageObject, PaginatedMixin):
"""
Library page in Studio
"""
diff --git a/common/test/acceptance/pages/studio/pagination.py b/common/test/acceptance/pages/studio/pagination.py
new file mode 100644
index 000000000000..a976149c37dd
--- /dev/null
+++ b/common/test/acceptance/pages/studio/pagination.py
@@ -0,0 +1,62 @@
+"""
+Mixin to include for Paginated container pages
+"""
+from selenium.webdriver.common.keys import Keys
+
+
+class PaginatedMixin(object):
+ """
+ Mixin class used for paginated page tests.
+ """
+ def nav_disabled(self, position, arrows=('next', 'previous')):
+ """
+ Verifies that pagination nav is disabled. Position can be 'top' or 'bottom'.
+
+ `top` is the header, `bottom` is the footer.
+
+ To specify a specific arrow, pass an iterable with a single element, 'next' or 'previous'.
+ """
+ return all([
+ self.q(css='nav.%s * a.%s-page-link.is-disabled' % (position, arrow))
+ for arrow in arrows
+ ])
+
+ def move_back(self, position):
+ """
+ Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'.
+ """
+ self.q(css='nav.%s * a.previous-page-link' % position)[0].click()
+ self.wait_until_ready()
+
+ def move_forward(self, position):
+ """
+ Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'.
+ """
+ self.q(css='nav.%s * a.next-page-link' % position)[0].click()
+ self.wait_until_ready()
+
+ def go_to_page(self, number):
+ """
+ Enter a number into the page number input field, and then try to navigate to it.
+ """
+ page_input = self.q(css="#page-number-input")[0]
+ page_input.click()
+ page_input.send_keys(str(number))
+ page_input.send_keys(Keys.RETURN)
+ self.wait_until_ready()
+
+ def get_page_number(self):
+ """
+ Returns the page number as the page represents it, in string form.
+ """
+ return self.q(css="span.current-page")[0].get_attribute('innerHTML')
+
+ def check_page_unchanged(self, first_block_name):
+ """
+ Used to make sure that a page has not transitioned after a bogus number is given.
+ """
+ if not self.xblocks[0].name == first_block_name:
+ return False
+ if not self.q(css='#page-number-input')[0].get_attribute('value') == '':
+ return False
+ return True
diff --git a/common/test/acceptance/tests/studio/test_studio_library.py b/common/test/acceptance/tests/studio/test_studio_library.py
index b505ac140dbc..491c9093d0fc 100644
--- a/common/test/acceptance/tests/studio/test_studio_library.py
+++ b/common/test/acceptance/tests/studio/test_studio_library.py
@@ -1,11 +1,15 @@
"""
Acceptance tests for Content Libraries in Studio
"""
+from ddt import ddt, data
+
from .base_studio_test import StudioLibraryTest
+from ...fixtures.course import XBlockFixtureDesc
from ...pages.studio.utils import add_component
from ...pages.studio.library import LibraryPage
+@ddt
class LibraryEditPageTest(StudioLibraryTest):
"""
Test the functionality of the library edit page.
@@ -107,3 +111,198 @@ def test_no_discussion_button(self):
Ensure the UI is not loaded for adding discussions.
"""
self.assertFalse(self.browser.find_elements_by_css_selector('span.large-discussion-icon'))
+
+ def test_library_pagination(self):
+ """
+ Scenario: Ensure that adding several XBlocks to a library results in pagination.
+ Given that I have a library in Studio with no XBlocks
+ And I create 10 Multiple Choice XBlocks
+ Then 10 are displayed.
+ When I add one more Multiple Choice XBlock
+ Then 1 XBlock will be displayed
+ When I delete that XBlock
+ Then 10 are displayed.
+ """
+ self.assertEqual(len(self.lib_page.xblocks), 0)
+ for _ in range(0, 10):
+ add_component(self.lib_page, "problem", "Multiple Choice")
+ self.assertEqual(len(self.lib_page.xblocks), 10)
+ add_component(self.lib_page, "problem", "Multiple Choice")
+ self.assertEqual(len(self.lib_page.xblocks), 1)
+ self.lib_page.click_delete_button(self.lib_page.xblocks[0].locator)
+ self.assertEqual(len(self.lib_page.xblocks), 10)
+
+ @data('top', 'bottom')
+ def test_nav_present_but_disabled(self, position):
+ """
+ Scenario: Ensure that the navigation buttons aren't active when there aren't enough XBlocks.
+ Given that I have a library in Studio with no XBlocks
+ The Navigation buttons should be disabled.
+ When I add a multiple choice problem
+ The Navigation buttons should be disabled.
+ """
+ self.assertEqual(len(self.lib_page.xblocks), 0)
+ self.assertTrue(self.lib_page.nav_disabled(position))
+ add_component(self.lib_page, "problem", "Multiple Choice")
+ self.assertTrue(self.lib_page.nav_disabled(position))
+
+
+@ddt
+class LibraryNavigationTest(StudioLibraryTest):
+ """
+ Test common Navigation actions
+ """
+ def setUp(self): # pylint: disable=arguments-differ
+ """
+ Ensure a library exists and navigate to the library edit page.
+ """
+ super(LibraryNavigationTest, self).setUp(is_staff=True)
+ self.lib_page = LibraryPage(self.browser, self.library_key)
+ self.lib_page.visit()
+ self.lib_page.wait_until_ready()
+
+ def populate_library_fixture(self, library_fixture):
+ """
+ Create four pages worth of XBlocks, and offset by one so each is named
+ after the number they should be in line by the user's perception.
+ """
+ # pylint: disable=attribute-defined-outside-init
+ self.blocks = [XBlockFixtureDesc('html', str(i)) for i in xrange(1, 41)]
+ library_fixture.add_children(*self.blocks)
+
+ def test_arbitrary_page_selection(self):
+ """
+ Scenario: I can pick a specific page number of a Library at will.
+ Given that I have a library in Studio with 40 XBlocks
+ When I go to the 3rd page
+ The first XBlock should be the 21st XBlock
+ When I go to the 4th Page
+ The first XBlock should be the 31st XBlock
+ When I go to the 1st page
+ The first XBlock should be the 1st XBlock
+ When I go to the 2nd page
+ The first XBlock should be the 11th XBlock
+ """
+ self.lib_page.go_to_page(3)
+ self.assertEqual(self.lib_page.xblocks[0].name, '21')
+ self.lib_page.go_to_page(4)
+ self.assertEqual(self.lib_page.xblocks[0].name, '31')
+ self.lib_page.go_to_page(1)
+ self.assertEqual(self.lib_page.xblocks[0].name, '1')
+ self.lib_page.go_to_page(2)
+ self.assertEqual(self.lib_page.xblocks[0].name, '11')
+
+ def test_bogus_page_selection(self):
+ """
+ Scenario: I can't pick a nonsense page number of a Library
+ Given that I have a library in Studio with 40 XBlocks
+ When I attempt to go to the 'a'th page
+ The input field will be cleared and no change of XBlocks will be made
+ When I attempt to visit the 5th page
+ The input field will be cleared and no change of XBlocks will be made
+ When I attempt to visit the -1st page
+ The input field will be cleared and no change of XBlocks will be made
+ When I attempt to visit the 0th page
+ The input field will be cleared and no change of XBlocks will be made
+ """
+ self.assertEqual(self.lib_page.xblocks[0].name, '1')
+ self.lib_page.go_to_page('a')
+ self.assertTrue(self.lib_page.check_page_unchanged('1'))
+ self.lib_page.go_to_page(-1)
+ self.assertTrue(self.lib_page.check_page_unchanged('1'))
+ self.lib_page.go_to_page(5)
+ self.assertTrue(self.lib_page.check_page_unchanged('1'))
+ self.lib_page.go_to_page(0)
+ self.assertTrue(self.lib_page.check_page_unchanged('1'))
+
+ @data('top', 'bottom')
+ def test_nav_buttons(self, position):
+ """
+ Scenario: Ensure that the navigation buttons work.
+ Given that I have a library in Studio with 40 XBlocks
+ The previous button should be disabled.
+ The first XBlock should be the 1st XBlock
+ Then if I hit the next button
+ The first XBlock should be the 11th XBlock
+ Then if I hit the next button
+ The first XBlock should be the 21st XBlock
+ Then if I hit the next button
+ The first XBlock should be the 31st XBlock
+ And the next button should be disabled
+ Then if I hit the previous button
+ The first XBlock should be the 21st XBlock
+ Then if I hit the previous button
+ The first XBlock should be the 11th XBlock
+ Then if I hit the previous button
+ The first XBlock should be the 1st XBlock
+ And the previous button should be disabled
+ """
+ # Check forward navigation
+ self.assertTrue(self.lib_page.nav_disabled(position, ['previous']))
+ self.assertEqual(self.lib_page.xblocks[0].name, '1')
+ self.lib_page.move_forward(position)
+ self.assertEqual(self.lib_page.xblocks[0].name, '11')
+ self.lib_page.move_forward(position)
+ self.assertEqual(self.lib_page.xblocks[0].name, '21')
+ self.lib_page.move_forward(position)
+ self.assertEqual(self.lib_page.xblocks[0].name, '31')
+ self.lib_page.nav_disabled(position, ['next'])
+
+ # Check backward navigation
+ self.lib_page.move_back(position)
+ self.assertEqual(self.lib_page.xblocks[0].name, '21')
+ self.lib_page.move_back(position)
+ self.assertEqual(self.lib_page.xblocks[0].name, '11')
+ self.lib_page.move_back(position)
+ self.assertEqual(self.lib_page.xblocks[0].name, '1')
+ self.assertTrue(self.lib_page.nav_disabled(position, ['previous']))
+
+ def test_library_pagination(self):
+ """
+ Scenario: Ensure that adding several XBlocks to a library results in pagination.
+ Given that I have a library in Studio with 40 XBlocks
+ Then 10 are displayed
+ And the first XBlock will be the 1st one
+ And I'm on the 1st page
+ When I add 1 Multiple Choice XBlock
+ Then 1 XBlock will be displayed
+ And I'm on the 5th page
+ The first XBlock will be the newest one
+ When I delete that XBlock
+ Then 10 are displayed
+ And I'm on the 4th page
+ And the first XBlock is the 31st one
+ And the last XBlock is the 40th one.
+ """
+ self.assertEqual(len(self.lib_page.xblocks), 10)
+ self.assertEqual(self.lib_page.get_page_number(), '1')
+ self.assertEqual(self.lib_page.xblocks[0].name, '1')
+ add_component(self.lib_page, "problem", "Multiple Choice")
+ self.assertEqual(len(self.lib_page.xblocks), 1)
+ self.assertEqual(self.lib_page.get_page_number(), '5')
+ self.assertEqual(self.lib_page.xblocks[0].name, "Multiple Choice")
+ self.lib_page.click_delete_button(self.lib_page.xblocks[0].locator)
+ self.assertEqual(len(self.lib_page.xblocks), 10)
+ self.assertEqual(self.lib_page.get_page_number(), '4')
+ self.assertEqual(self.lib_page.xblocks[0].name, '31')
+ self.assertEqual(self.lib_page.xblocks[-1].name, '40')
+
+ def test_delete_shifts_blocks(self):
+ """
+ Scenario: Ensure that removing an XBlock shifts other blocks back.
+ Given that I have a library in Studio with 40 XBlocks
+ Then 10 are displayed
+ And I will be on the first page
+ When I delete the third XBlock
+ There will be 10 displayed
+ And the first XBlock will be the first one
+ And the last XBlock will be the 11th one
+ And I will be on the first page
+ """
+ self.assertEqual(len(self.lib_page.xblocks), 10)
+ self.assertEqual(self.lib_page.get_page_number(), '1')
+ self.lib_page.click_delete_button(self.lib_page.xblocks[2].locator, confirm=True)
+ self.assertEqual(len(self.lib_page.xblocks), 10)
+ self.assertEqual(self.lib_page.xblocks[0].name, '1')
+ self.assertEqual(self.lib_page.xblocks[-1].name, '11')
+ self.assertEqual(self.lib_page.get_page_number(), '1')
diff --git a/lms/templates/studio_render_paged_children_view.html b/lms/templates/studio_render_paged_children_view.html
new file mode 100644
index 000000000000..fe5b5403e1ab
--- /dev/null
+++ b/lms/templates/studio_render_paged_children_view.html
@@ -0,0 +1,23 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+<%namespace name='static' file='static_content.html'/>
+
+% for template_name in ["paging-header", "paging-footer"]:
+
+% endfor
+
+
+
+
+
+% for item in items:
+ ${item['content']}
+% endfor
+
+% if can_add:
+
+% endif
+
+