Index: trunk/roundpages/pagetree.py
===================================================================
--- trunk/roundpages/pagetree.py (revision 227)
+++ trunk/roundpages/pagetree.py (revision 227)
@@ -0,0 +1,64 @@
+from pathobjtree import PathObjectNode, PathObjectTree
+
+
+__doc__ = r"""
+    >>> class TestPage:
+    ...     def __init__(self, path, index):
+    ...         self.path, self.index = path, index
+    ...     def __repr__(self): return '%d:%s' % (self.index, self.path)
+
+    >>> tree = PageTree() ; print tree
+    <PageTree ()>
+
+    >>> tree.add(TestPage('/left/top/', 1)) ; print tree
+    <PageTree ()>
+     <GeneratedPathNode ('left',)>
+      <PageNode 1:/left/top/>
+
+    Automatically generated nodes appear after real nodes.
+
+    >>> tree.add(TestPage('/center/', 2)) ; print tree
+    <PageTree ()>
+     <PageNode 2:/center/>
+     <GeneratedPathNode ('left',)>
+      <PageNode 1:/left/top/>
+
+    Now the generated node is replaced by a real node with a lower index.
+
+    >>> tree.add(TestPage('/left/', 1)) ; print tree
+    <PageTree ()>
+     <PageNode 1:/left/>
+      <PageNode 1:/left/top/>
+     <PageNode 2:/center/>
+"""
+
+
+def normalize_path(path):
+    return '/%s/' % path.strip('/')
+
+
+class PageNode(PathObjectNode):
+    def _get_path_chain(self):
+        if self.path is None:
+            # no content, no children => no PATH => no path_parts
+            return []
+        return tuple(self.path[1:-1].split('/'))
+    path_parts = property(_get_path_chain)
+
+    def _get_path(self):
+        return normalize_path(self.object.path)
+    path = property(_get_path)
+
+    def _get_index(self):
+        return self.object.navigation_order
+    index = property(_get_index)
+
+
+class PageTree(PathObjectTree):
+    def __init__(self):
+        super(PageTree, self).__init__(PageNode)
+
+
+if __name__ == '__main__':
+    from doctest import testmod
+    testmod()
Index: trunk/roundpages/views.py
===================================================================
--- trunk/roundpages/views.py (revision 227)
+++ trunk/roundpages/views.py (revision 227)
@@ -0,0 +1,94 @@
+from django.http import HttpResponseRedirect
+from django.conf import settings
+from django.template import RequestContext
+from django.shortcuts import get_object_or_404, render_to_response
+from django.utils.translation import get_language
+from django.core.urlresolvers import reverse
+from django.contrib.auth.decorators import permission_required
+
+from models import RoundPage, RoundPageTranslation
+from forms import \
+     EditRoundPageForm, EditRoundPageTranslationForm, \
+     NewRoundPageForm, NewRoundPageTranslationForm
+from pagetree import PageTree
+
+@permission_required('roundpages.can_edit')
+def edit(request, path=None, *args, **kwargs):
+    try:
+        page = RoundPage.objects.get(path=path)
+        template_name = page.template_name
+    except RoundPage.DoesNotExist:
+        page = RoundPage(path=path)
+        template_name = 'roundpages/default.html'
+    try:
+        translation = page.translations.get(language=get_language())
+    except RoundPageTranslation.DoesNotExist:
+        translation = RoundPageTranslation(page=page, language=get_language())
+
+    if request.method == 'POST':
+        page_form = EditRoundPageForm(request.POST,
+                                      instance=page)
+        translation_form = EditRoundPageTranslationForm(request.POST,
+                                                        instance=translation)
+        if page_form.is_valid() and translation_form.is_valid():
+            translation.page = page_form.save()
+            translation = translation_form.save()
+            kwargs['path'] = page.path
+            return HttpResponseRedirect(reverse('view-roundpage',
+                                                kwargs=kwargs))
+    else:
+        page_form = EditRoundPageForm(instance=page)
+        translation_form = EditRoundPageTranslationForm(instance=translation)
+
+    return render_to_response(
+        template_name,
+        dict(page_form=page_form,
+             translation_form=translation_form,
+             roundpages_edit=True),
+        RequestContext(request))
+
+
+def view(request, path, **kwargs):
+    translation = get_object_or_404(
+        RoundPageTranslation,
+        page__path=path,
+        language=get_language())
+    return render_to_response(
+        translation.page.template_name,
+        dict(translation=translation, page=translation.page),
+        RequestContext(request))
+
+
+def pagemap(request, **kwargs):
+    tree = PageTree()
+    for page in RoundPage.objects.all().select_related(True):
+        tree.add(page)
+    edit=request.user.has_perm('roundpages.can_edit')
+    context = dict(tree=tree,
+                   roundpages_edit=edit,
+                   language_codes=[item[0] for item in settings.LANGUAGES])
+    if edit:
+        page = RoundPage()
+        translation = RoundPageTranslation(page=page, language=get_language())
+        if request.method == 'POST':
+            page_form = NewRoundPageForm(
+                request.POST, instance=page)
+            translation_form = NewRoundPageTranslationForm(
+                request.POST, instance=translation)
+            if page_form.is_valid() and translation_form.is_valid():
+                translation.page = page_form.save()
+                translation = translation_form.save()
+                kwargs['path'] = page.path
+                return HttpResponseRedirect(reverse('view-roundpage',
+                                                    kwargs=kwargs))
+        else:
+            page_form = NewRoundPageForm(
+                instance=page)
+            translation_form = NewRoundPageTranslationForm(
+                instance=translation)
+        context['page_form'] = page_form
+        context['translation_form'] = translation_form
+    return render_to_response(
+        'roundpages/pagemap.html',
+        context,
+        RequestContext(request))
Index: trunk/roundpages/models.py
===================================================================
--- trunk/roundpages/models.py (revision 227)
+++ trunk/roundpages/models.py (revision 227)
@@ -0,0 +1,51 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _, get_language
+from django.conf.language_names import language_choices
+
+
+class RoundPage(models.Model):
+
+    path = models.CharField(max_length=200,
+                            help_text=_('No leading or trailing slash'),
+                            unique=True)
+    navigation_order = models.IntegerField(default=0)
+    template_name = models.CharField(max_length=200,
+                                     default='roundpages/default.html')
+
+    def in_current_language(self):
+        return self.translations.in_current_language()
+
+    def __str__(self):
+        return self.path
+
+
+class RoundPageTranslationManager(models.Manager):
+    def get_query_set(self):
+        return super(RoundPageTranslationManager, self) \
+               .get_query_set() \
+               .select_related(True)
+
+    def in_current_language(self):
+        return self.get_query_set().get(language=get_language())
+
+
+class RoundPageTranslationActiveLanguageManager(RoundPageTranslationManager):
+    def get_query_set(self):
+        return super(RoundPageTranslationActiveLanguageManager, self) \
+               .get_query_set().filter(language=get_language())
+
+
+class RoundPageTranslation(models.Model):
+
+    page = models.ForeignKey(RoundPage, related_name='translations')
+    language = models.CharField(max_length=5,
+                                choices=language_choices)
+    title = models.CharField(max_length=200, blank=True)
+    content = models.TextField(blank=True)
+
+    objects = RoundPageTranslationManager()
+    in_current_language = RoundPageTranslationActiveLanguageManager()
+
+    class Meta:
+        unique_together = 'page', 'language',
+        ordering = 'roundpages_roundpage.navigation_order',
Index: trunk/roundpages/search.py
===================================================================
--- trunk/roundpages/search.py (revision 227)
+++ trunk/roundpages/search.py (revision 227)
@@ -0,0 +1,2 @@
+def search_roundpages(request):
+    return []
Index: trunk/roundpages/urls.py
===================================================================
--- trunk/roundpages/urls.py (revision 227)
+++ trunk/roundpages/urls.py (revision 227)
@@ -0,0 +1,18 @@
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns(
+    'ambidjangolib.roundpages.views',
+
+    url(r'^pagemap/',
+        'pagemap',
+        name='roundpages-map'),
+
+    url(r'^(?P<path>.*)/edit/',
+        'edit',
+        name='edit-roundpage'),
+
+    url(r'^(?P<path>.*)/',
+        'view',
+        name='view-roundpage')
+)
Index: trunk/roundpages/forms.py
===================================================================
--- trunk/roundpages/forms.py (revision 227)
+++ trunk/roundpages/forms.py (revision 227)
@@ -0,0 +1,25 @@
+from django import newforms as forms
+
+from models import RoundPage, RoundPageTranslation
+
+
+class EditRoundPageForm(forms.ModelForm):
+    class Meta:
+        model = RoundPage
+        fields = 'path',
+
+class EditRoundPageTranslationForm(forms.ModelForm):
+    class Meta:
+        model = RoundPageTranslation
+        fields = 'title', 'content',
+
+
+class NewRoundPageForm(forms.ModelForm):
+    class Meta:
+        model = RoundPage
+        fields = 'path',
+
+class NewRoundPageTranslationForm(forms.ModelForm):
+    class Meta:
+        model = RoundPageTranslation
+        fields = 'title', 'content',
Index: trunk/roundpages/pathobjtree.py
===================================================================
--- trunk/roundpages/pathobjtree.py (revision 227)
+++ trunk/roundpages/pathobjtree.py (revision 227)
@@ -0,0 +1,190 @@
+from bisect import insort
+
+
+__doc__ = r"""
+    >>> class TestObject:
+    ...     def __init__(self, *path_parts):
+    ...         self.path_parts = path_parts
+    ...     def __repr__(self): return repr(self.path_parts)
+    >>> class TestNode(PathObjectNode):
+    ...     def _get_path_chain(self):
+    ...         return self.object.path_parts
+    ...     path_parts = property(_get_path_chain)
+
+
+    Empty tree
+    ==========
+
+    >>> tree = PathObjectTree(TestNode) ; print tree, tree.level
+    <PathObjectTree ()> 0
+
+
+    Generated first-level node
+    ==========================
+
+    >>> tree.add_node(GeneratedPathNode((1,)))
+    <GeneratedPathNode (1,)>
+
+    >>> print tree
+    <PathObjectTree ()>
+     <GeneratedPathNode (1,)>
+
+
+    Real second-level node below generated first-level node
+    =======================================================
+
+    >>> node = TestNode(TestObject(1, 2)) ; print repr(node), node.level
+    <TestNode (1, 2)> 2
+
+    >>> tree.find_branch_for(node)
+    <GeneratedPathNode (1,)>
+
+    >>> tree.add_node(node)
+    <TestNode (1, 2)>
+
+    >>> print tree
+    <PathObjectTree ()>
+     <GeneratedPathNode (1,)>
+      <TestNode (1, 2)>
+
+
+    Real second-level node below a non-existing first-level node
+    ============================================================
+
+    >>> node = TestNode(TestObject(3, 4)) ; node.level
+    2
+
+    >>> tree.find_branch_for(node)
+    Traceback (most recent call last):
+    ValueError: Branch for <TestNode (3, 4)> not found
+
+    >>> tree.add_node(node)
+    <TestNode (3, 4)>
+
+    >>> print tree
+    <PathObjectTree ()>
+     <GeneratedPathNode (1,)>
+      <TestNode (1, 2)>
+     <GeneratedPathNode (3,)>
+      <TestNode (3, 4)>
+
+
+    Real first-level node replacing a generated first-level node
+    ============================================================
+
+    >>> node = TestNode(TestObject(3)) ; node.level
+    1
+
+    >>> tree.add_node(node)
+    <TestNode (3,)>
+
+    >>> print tree
+    <PathObjectTree ()>
+     <GeneratedPathNode (1,)>
+      <TestNode (1, 2)>
+     <TestNode (3,)>
+      <TestNode (3, 4)>
+"""
+
+class BasePathNode(object):
+    """
+    The ``BasePathNode`` abstract class defines a tree of children and can add
+    objects to it for instances of classes inherited from ``BasePathNode``.
+    """
+    def __init__(self):
+        self.children = []
+
+    def add_node(self, node):
+        if node.level <= self.level:
+            raise ValueError("Can't add lower level node under a deeper node")
+        try:
+            branch = self.find_branch_for(node)
+        except ValueError:
+            branch = None
+        if node.level == self.level + 1:
+            if branch:
+                if isinstance(branch, GeneratedPathNode):
+                    node.children = branch.children
+                    self.children.remove(branch)
+                else:
+                    raise ValueError("Duplicate node %r" % node)
+            insort(self.children, node)
+        else:
+            if not branch:
+                branch = self.add_node(GeneratedPathNode(
+                    node.path_parts[:self.level+1]))
+            branch.add_node(node)
+        return node
+
+    def add(self, obj, node_class):
+        self.add_node(node_class(obj))
+
+    def find_branch_for(self, node):
+        for child in self.children:
+            if child.path_parts == node.path_parts[:child.level]:
+                return child
+        raise ValueError("Branch for %r not found" % node)
+
+    def _get_level(self):
+        return len(self.path_parts)
+    level = property(_get_level)
+
+    def __cmp__(self, other):
+        return cmp(self.index, other.index)
+
+    def __str__(self):
+        rows = [self.level * ' ' + repr(self)]
+        for child in self.children:
+            rows.append(str(child))
+        return '\n'.join(rows)
+
+    def __repr__(self):
+        return '<%s %r>' % (self.__class__.__name__, self.path_parts)
+
+
+class GeneratedPathNode(BasePathNode):
+    """
+    A ``GeneratedPathNode`` instance knows its path parts.
+    """
+    index = 'last'  # strings come after interegers
+    def __init__(self, path_parts):
+        super(GeneratedPathNode, self).__init__()
+        self.path_parts = tuple(path_parts)
+
+
+class PathObjectNode(BasePathNode):
+    """
+    The ``PathObjectNode`` abstract class wraps objects.  Inherited classes
+    must implement the ``path_parts`` property and resolve path parts from the
+    object.
+    """
+    def __init__(self, obj):
+        super(PathObjectNode, self).__init__()
+        self.object = obj
+
+    def _get_index(self):
+        """
+        By default, sort according to the last part of the path.
+        """
+        return self.path_parts[-1]
+    index = property(_get_index)
+
+    def __repr__(self):
+        return '<%s %r>' % (self.__class__.__name__, self.object)
+
+
+class PathObjectTree(BasePathNode):
+    path_parts = ()
+
+    def __init__(self, node_class):
+        super(PathObjectTree, self).__init__()
+        self.node_class = node_class
+
+    def add(self, obj):
+        super(PathObjectTree, self).add(obj, self.node_class)
+
+
+
+if __name__ == '__main__':
+    from doctest import testmod
+    testmod()
Index: trunk/roundpages/templates/roundpages/default.html
===================================================================
--- trunk/roundpages/templates/roundpages/default.html (revision 227)
+++ trunk/roundpages/templates/roundpages/default.html (revision 227)
@@ -0,0 +1,1 @@
+{% extends "roundpages/base.html" %}
Index: trunk/roundpages/templates/roundpages/edit-form.html
===================================================================
--- trunk/roundpages/templates/roundpages/edit-form.html (revision 227)
+++ trunk/roundpages/templates/roundpages/edit-form.html (revision 227)
@@ -0,0 +1,6 @@
+{% load i18n %}
+<form method="post" action="." />
+  {{ page_form.as_ul }}
+  {{ translation_form.as_ul }}
+  <input type="submit" value="{% trans "Save changes" %}" />
+</form>
Index: trunk/roundpages/templates/roundpages/pagemap.html
===================================================================
--- trunk/roundpages/templates/roundpages/pagemap.html (revision 227)
+++ trunk/roundpages/templates/roundpages/pagemap.html (revision 227)
@@ -0,0 +1,43 @@
+{% extends "roundpages/base.html" %}
+{% load i18n recurse %}
+
+{% block body_content %}
+  {% if roundpages_edit %}
+    <form method="post" action=".">
+  {% endif %}
+  {% recurse page.children with tree.children as page %}
+    <ul>
+      {% loop %}
+        <li>
+	  {% if roundpages_edit %}
+	    <span class="title">{{ page.object.in_current_language.title }}</span>
+	    <span class="edit-in-languages">
+	    {% for langcode in language_codes %}
+	      <a href="{% url edit-roundpage langcode=langcode,path=page.object.path %}">{{ langcode }}</a>
+            {% endfor %}{# langcode in language_codes #}
+	    </span>
+
+	    <span class="delete">
+	      <input type="checkbox" name="delete_{{page.object.pk}}" />
+	    </span>
+	  {% else %}{# roundpages_edit #}
+	    <a href="{% url view-roundpage langcode=LANGUAGE_CODE,path=page.object.path %}">{{ page.object.in_current_language.title }}</a>
+	  {% endif %}{# roundpages_edit #}
+	
+          {% child %}
+        </li>
+      {% endloop %}
+    </ul>
+  {% endrecurse %}
+
+  {% if roundpages_edit %}
+    <h1>{% trans "New page" %}</h1>
+    {% include "roundpages/photochooser.html" %}
+    {{ page_form.as_ul }}
+    {{ translation_form.as_ul }}
+    {% include "roundpages/yui-editor.html" %}
+    <input type="submit" value="{% trans "Save changes" %}" />
+    </form>
+  {% endif %}
+
+{% endblock body_content %}
Index: trunk/dbpickle/dbpickle.py
===================================================================
--- trunk/dbpickle/dbpickle.py (revision 212)
+++ trunk/dbpickle/dbpickle.py (revision 227)
@@ -140,6 +140,11 @@
     models = set( [obj.__class__ for obj in objects.itervalues()] )
     for model in models:
-        for obj in model._default_manager.all():
-            obj.delete()
+        try:
+            for obj in model._default_manager.all():
+                obj.delete()
+        except (backend.Database.OperationalError,
+                backend.Database.ProgrammingError), e:
+            logging.warning('table for %r not found (%s)' % (model, e))
+            transaction.rollback_unless_managed()
 
     # load all objects
