From 54a62999c03f5293b42d56f9a99b1c5a3b02fa5f Mon Sep 17 00:00:00 2001
From: substantialnoninfringinguser <substantialnoninfringinguser@gmail.com>
Date: Thu, 21 May 2009 19:47:19 +0100
Subject: [PATCH] [svn] Updates to allow subcave tree with nice admin.

---
 databaseReset.py                              |   4 +-
 expo/admin.py                                 |   9 +-
 expo/models.py                                |  57 ++--
 feincms/__init__.py                           |   0
 feincms/admin/__init__.py                     |  12 +
 feincms/admin/editor.py                       | 183 ++++++++++++
 feincms/content/__init__.py                   |   0
 feincms/content/file/__init__.py              |   0
 feincms/content/file/models.py                |  20 ++
 feincms/content/image/__init__.py             |   0
 feincms/content/image/models.py               |  32 ++
 feincms/content/richtext/__init__.py          |   0
 feincms/content/richtext/models.py            |  16 +
 feincms/content/rss/__init__.py               |   0
 feincms/content/rss/models.py                 |  39 +++
 feincms/content/video/__init__.py             |   0
 feincms/content/video/models.py               |  26 ++
 feincms/locale/de/LC_MESSAGES/django.po       | 281 ++++++++++++++++++
 feincms/management/__init__.py                |   0
 feincms/management/commands/__init__.py       |   0
 .../management/commands/update_rsscontent.py  |  12 +
 feincms/models.py                             | 224 ++++++++++++++
 feincms/module/__init__.py                    |   0
 feincms/module/page/__init__.py               |   0
 feincms/module/page/admin.py                  |  33 ++
 feincms/module/page/models.py                 | 232 +++++++++++++++
 feincms/module/page/templatetags/__init__.py  |   0
 .../page/templatetags/feincms_page_tags.py    |  63 ++++
 feincms/shortcuts.py                          |  13 +
 .../templates/admin/feincms/item_editor.html  | 227 ++++++++++++++
 .../templates/admin/feincms/tree_editor.html  | 145 +++++++++
 feincms/templates/content/rss/content.html    |   8 +
 feincms/templatetags/__init__.py              |   0
 feincms/templatetags/feincms_tags.py          |  75 +++++
 feincms/templatetags/utils.py                 | 150 ++++++++++
 feincms/views/__init__.py                     |   0
 feincms/views/base.py                         |  23 ++
 feincms/views/decorators.py                   |  16 +
 feincms/views/generic/__init__.py             |   0
 feincms/views/generic/create_update.py        |   8 +
 feincms/views/generic/date_based.py           |  12 +
 feincms/views/generic/list_detail.py          |   7 +
 feincms/views/generic/simple.py               |   6 +
 parsers/surveys.py                            |  25 +-
 settings.py                                   |   2 +
 templates/survey.html                         |   4 +-
 46 files changed, 1931 insertions(+), 33 deletions(-)
 create mode 100644 feincms/__init__.py
 create mode 100644 feincms/admin/__init__.py
 create mode 100644 feincms/admin/editor.py
 create mode 100644 feincms/content/__init__.py
 create mode 100644 feincms/content/file/__init__.py
 create mode 100644 feincms/content/file/models.py
 create mode 100644 feincms/content/image/__init__.py
 create mode 100644 feincms/content/image/models.py
 create mode 100644 feincms/content/richtext/__init__.py
 create mode 100644 feincms/content/richtext/models.py
 create mode 100644 feincms/content/rss/__init__.py
 create mode 100644 feincms/content/rss/models.py
 create mode 100644 feincms/content/video/__init__.py
 create mode 100644 feincms/content/video/models.py
 create mode 100644 feincms/locale/de/LC_MESSAGES/django.po
 create mode 100644 feincms/management/__init__.py
 create mode 100644 feincms/management/commands/__init__.py
 create mode 100644 feincms/management/commands/update_rsscontent.py
 create mode 100644 feincms/models.py
 create mode 100644 feincms/module/__init__.py
 create mode 100644 feincms/module/page/__init__.py
 create mode 100644 feincms/module/page/admin.py
 create mode 100644 feincms/module/page/models.py
 create mode 100644 feincms/module/page/templatetags/__init__.py
 create mode 100644 feincms/module/page/templatetags/feincms_page_tags.py
 create mode 100644 feincms/shortcuts.py
 create mode 100644 feincms/templates/admin/feincms/item_editor.html
 create mode 100644 feincms/templates/admin/feincms/tree_editor.html
 create mode 100644 feincms/templates/content/rss/content.html
 create mode 100644 feincms/templatetags/__init__.py
 create mode 100644 feincms/templatetags/feincms_tags.py
 create mode 100644 feincms/templatetags/utils.py
 create mode 100644 feincms/views/__init__.py
 create mode 100644 feincms/views/base.py
 create mode 100644 feincms/views/decorators.py
 create mode 100644 feincms/views/generic/__init__.py
 create mode 100644 feincms/views/generic/create_update.py
 create mode 100644 feincms/views/generic/date_based.py
 create mode 100644 feincms/views/generic/list_detail.py
 create mode 100644 feincms/views/generic/simple.py

diff --git a/databaseReset.py b/databaseReset.py
index 748d0e3..ed24a3e 100644
--- a/databaseReset.py
+++ b/databaseReset.py
@@ -14,7 +14,7 @@ def reload_db():
     cursor.execute("create database %s" % settings.DATABASE_NAME)
     cursor.execute("ALTER DATABASE %s CHARACTER SET=utf8" % settings.DATABASE_NAME)
     cursor.execute("USE %s" % settings.DATABASE_NAME)
-    management.call_command('syncdb')
+    management.call_command('syncdb', interactive=False)
     user = User.objects.create_user('m', 'm@m.com', 'm')
     user.is_staff = True
     user.is_superuser = True
@@ -50,6 +50,8 @@ def import_surveys():
     parsers.surveys.parseSurveys(logfile=settings.LOGFILE)
 
 def reset():
+    """ Wipe the troggle database and import everything from legacy data
+    """
     reload_db()
     make_dirs()
     import_cavetab()
diff --git a/expo/admin.py b/expo/admin.py
index f2717d9..3b727d2 100644
--- a/expo/admin.py
+++ b/expo/admin.py
@@ -1,5 +1,6 @@
 from troggle.expo.models import *
 from django.contrib import admin
+from feincms.admin import editor
 from django.forms import ModelForm
 import django.forms as forms
 from expo.forms import LogbookEntryForm
@@ -69,10 +70,12 @@ class CaveAdmin(TroggleModelAdmin):
     #inlines = (QMInline,)
     extra = 4
 
+class SubcaveAdmin(editor.TreeEditorMixin,TroggleModelAdmin):
+    pass
 
 
 admin.site.register(Photo)
-admin.site.register(Subcave)
+admin.site.register(Subcave, SubcaveAdmin)
 admin.site.register(Cave, CaveAdmin)
 admin.site.register(Area)
 admin.site.register(OtherCaveName)
@@ -91,3 +94,7 @@ admin.site.register(QM, QMAdmin)
 admin.site.register(Survey, SurveyAdmin)
 admin.site.register(ScannedImage)
 
+try:
+    mptt.register(Subcave, order_insertion_by=['name'])
+except mptt.AlreadyRegistered:
+    print "mptt already registered"
diff --git a/expo/models.py b/expo/models.py
index 798fa35..ece0e26 100644
--- a/expo/models.py
+++ b/expo/models.py
@@ -1,4 +1,5 @@
 import urllib, urlparse, string, os, datetime
+import troggle.mptt as mptt
 from django.forms import ModelForm
 from django.db import models
 from django.contrib import admin
@@ -463,31 +464,35 @@ class Entrance(TroggleModel):
                 return f[1]
                 
 class Subcave(TroggleModel):
-    description = models.TextField()
-    name = models.CharField(max_length=200, )
+    description = models.TextField(blank=True, null=True)
+    title = models.CharField(max_length=200, )
     cave = models.ForeignKey('Cave', blank=True, null=True, help_text="Only the top-level subcave should be linked to a cave")
-    parent= models.ForeignKey('Subcave', blank=True, null=True, related_name='children')
-    adjoining = models.ManyToManyField('Subcave',blank=True, null=True,)
-    survex_file = models.CharField(max_length=200, blank=True, null=True,)
-    
+    parent = models.ForeignKey('self', null=True, blank=True, related_name='children')
+    #adjoining = models.ManyToManyField('Subcave',blank=True, null=True,)
+    legacy_description_path = models.CharField(max_length=600, blank=True, null=True)
     def __unicode__(self):
-        return self.name
+        return self.title
     
-    def get_absolute_url(self):
-        urlString=self.name
-        if self.parent:
-            parent=self.parent
-            while parent: #recursively walk up the tree, adding parents to the left of the URL
-                urlString=parent.name+'/'+urlString
-                if parent.cave:
-                    cave=parent.cave
-                parent=parent.parent
-            urlString='cave/'+unicode(cave.kataster_number)+'/'+urlString
-        else:
-            urlString='cave/'+unicode(self.cave.kataster_number)+'/'+urlString
+
+#    def get_absolute_url(self):
+#        urlString=self.name
+#        if self.parent:
+#            parent=self.parent
+#            while parent: #recursively walk up the tree, adding parents to the left of the URL
+#                urlString=parent.name+'/'+urlString
+#                if parent.cave:
+#                    cave=parent.cave
+#                parent=parent.parent
+#            urlString='cave/'+unicode(cave.kataster_number)+'/'+urlString
+#        else:
+#            urlString='cave/'+unicode(self.cave.kataster_number)+'/'+urlString
             
             
-        return urlparse.urljoin(settings.URL_ROOT, urlString)
+#        return urlparse.urljoin(settings.URL_ROOT, urlString)
+try:
+    mptt.register(Subcave, order_insertion_by=['title'])
+except mptt.AlreadyRegistered:
+    print "mptt already registered"
 
 class QM(TroggleModel):
     #based on qm.csv in trunk/expoweb/smkridge/204 which has the fields:
@@ -592,13 +597,15 @@ class ScannedImage(TroggleImageModel):
         return get_scan_path(self,'')
 
 class Survey(TroggleModel):
-    expedition = models.ForeignKey('Expedition')
+    expedition = models.ForeignKey('Expedition') #REDUNDANT (logbook_entry)
     wallet_number = models.IntegerField(blank=True,null=True)
     wallet_letter = models.CharField(max_length=1,blank=True,null=True)
     comments = models.TextField(blank=True,null=True)
-    location = models.CharField(max_length=400,blank=True,null=True)
+    location = models.CharField(max_length=400,blank=True,null=True) #REDUNDANT
+    subcave = models.ForeignKey('Subcave', blank=True, null=True)
     #notes_scan = models.ForeignKey('ScannedImage',related_name='notes_scan',blank=True, null=True)  	#Replaced by contents field of ScannedImage model
-    survex_block  = models.ForeignKey('SurvexBlock',blank=True, null=True)
+    survex_block  = models.OneToOneField('SurvexBlock',blank=True, null=True)
+    logbook_entry = models.ForeignKey('LogbookEntry')
     centreline_printed_on = models.DateField(blank=True, null=True)
     centreline_printed_by = models.ForeignKey('Person',related_name='centreline_printed_by',blank=True,null=True)
     #sketch_scan = models.ForeignKey(ScannedImage,blank=True, null=True) 					#Replaced by contents field of ScannedImage model
@@ -608,7 +615,7 @@ class Survey(TroggleModel):
     integrated_into_main_sketch_by = models.ForeignKey('Person' ,related_name='integrated_into_main_sketch_by', blank=True,null=True)
     rendered_image = models.ImageField(upload_to='renderedSurveys',blank=True,null=True)
     def __str__(self):
-        return self.expedition.year+"#"+"%02d" % self.wallet_number
+        return self.expedition.year+"#"+"%02d" % int(self.wallet_number)
 
     def notes(self):
 	    return self.scannedimage_set.filter(contents='notes')
@@ -618,5 +625,3 @@ class Survey(TroggleModel):
 
     def elevations(self):
 	    return self.scannedimage_set.filter(contents='elevation')
-    
-    
diff --git a/feincms/__init__.py b/feincms/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/admin/__init__.py b/feincms/admin/__init__.py
new file mode 100644
index 0000000..5079662
--- /dev/null
+++ b/feincms/admin/__init__.py
@@ -0,0 +1,12 @@
+from django.contrib import admin
+
+from feincms.models import Region, Template
+
+
+admin.site.register(Region,
+    list_display=('title', 'key', 'inherited'),
+    )
+admin.site.register(Template,
+    list_display=('title', 'path'),
+    )
+
diff --git a/feincms/admin/editor.py b/feincms/admin/editor.py
new file mode 100644
index 0000000..c5c688b
--- /dev/null
+++ b/feincms/admin/editor.py
@@ -0,0 +1,183 @@
+import re
+
+from django import forms, template
+from django.conf import settings
+from django.contrib import admin
+from django.contrib.admin.options import IncorrectLookupParameters
+from django.contrib.admin.util import unquote
+from django.core import serializers
+from django.core.exceptions import ImproperlyConfigured
+from django.db import connection, transaction
+from django.forms.formsets import all_valid
+from django.forms.models import inlineformset_factory
+from django.http import HttpResponseRedirect, HttpResponse
+from django.shortcuts import render_to_response
+from django.utils import simplejson
+from django.utils.encoding import force_unicode
+from django.utils.functional import update_wrapper
+from django.utils.translation import ugettext_lazy as _
+
+
+
+FEINCMS_ADMIN_MEDIA = getattr(settings, 'FEINCMS_ADMIN_MEDIA', '/media/sys/feincms/')
+
+
+class ItemEditorMixin(object):
+    """
+    This mixin needs an attribute on the ModelAdmin class:
+
+    show_on_top::
+        A list of fields which should be displayed at the top of the form.
+        This does not need to (and should not) include ``template''
+    """
+
+    def change_view(self, request, object_id, extra_context=None):
+
+        if not hasattr(self.model, '_feincms_content_types'):
+            raise ImproperlyConfigured, 'You need to create at least one content type for the %s model.' % (self.model.__name__)
+
+        class ModelForm(forms.ModelForm):
+            class Meta:
+                model = self.model
+
+        class SettingsFieldset(forms.ModelForm):
+            # This form class is used solely for presentation, the data will be saved
+            # by the ModelForm above
+
+            class Meta:
+                model = self.model
+                exclude = self.show_on_top+('template',)
+
+        inline_formset_types = [(
+            content_type,
+            inlineformset_factory(self.model, content_type, extra=1)
+            ) for content_type in self.model._feincms_content_types]
+
+        opts = self.model._meta
+        app_label = opts.app_label
+        obj = self.model._default_manager.get(pk=unquote(object_id))
+
+        if not self.has_change_permission(request, obj):
+            raise PermissionDenied
+
+        if request.method == 'POST':
+            model_form = ModelForm(request.POST, request.FILES, instance=obj)
+
+            inline_formsets = [
+                formset_class(request.POST, request.FILES, instance=obj,
+                    prefix=content_type.__name__.lower())
+                for content_type, formset_class in inline_formset_types]
+
+            if model_form.is_valid() and all_valid(inline_formsets):
+                model_form.save()
+                for formset in inline_formsets:
+                    formset.save()
+                return HttpResponseRedirect(".")
+
+            settings_fieldset = SettingsFieldset(request.POST, instance=obj)
+            settings_fieldset.is_valid()
+        else:
+            model_form = ModelForm(instance=obj)
+            inline_formsets = [
+                formset_class(instance=obj, prefix=content_type.__name__.lower())
+                for content_type, formset_class in inline_formset_types]
+
+            settings_fieldset = SettingsFieldset(instance=obj)
+
+        content_types = []
+        for content_type in self.model._feincms_content_types:
+            content_name = content_type._meta.verbose_name
+            content_types.append((content_name, content_type.__name__.lower()))
+
+        context = {
+            'title': _('Change %s') % force_unicode(opts.verbose_name),
+            'opts': opts,
+            'page': obj,
+            'page_form': model_form,
+            'inline_formsets': inline_formsets,
+            'content_types': content_types,
+            'settings_fieldset': settings_fieldset,
+            'top_fieldset': [model_form[field] for field in self.show_on_top],
+            'FEINCMS_ADMIN_MEDIA': FEINCMS_ADMIN_MEDIA,
+        }
+
+        return render_to_response([
+            'admin/feincms/%s/%s/item_editor.html' % (app_label, opts.object_name.lower()),
+            'admin/feincms/%s/item_editor.html' % app_label,
+            'admin/feincms/item_editor.html',
+            ], context, context_instance=template.RequestContext(request))
+
+
+class TreeEditorMixin(object):
+    def changelist_view(self, request, extra_context=None):
+        # handle AJAX requests
+        if request.is_ajax():
+            cmd = request.POST.get('__cmd')
+            if cmd=='save_tree':
+                return self._save_tree(request)
+            elif cmd=='delete_item':
+                return self._delete_item(request)
+
+            return HttpResponse('Oops. AJAX request not understood.')
+
+        from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
+        opts = self.model._meta
+        app_label = opts.app_label
+
+        if not self.has_change_permission(request, None):
+            raise PermissionDenied
+        try:
+            cl = ChangeList(request, self.model, self.list_display,
+                self.list_display_links, self.list_filter, self.date_hierarchy,
+                self.search_fields, self.list_select_related, self.list_per_page,
+                self.list_editable, self)
+        except IncorrectLookupParameters:
+            # Wacky lookup parameters were given, so redirect to the main
+            # changelist page, without parameters, and pass an 'invalid=1'
+            # parameter via the query string. If wacky parameters were given and
+            # the 'invalid=1' parameter was already in the query string, something
+            # is screwed up with the database, so display an error page.
+            if ERROR_FLAG in request.GET.keys():
+                return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
+            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
+
+        context = {
+            'FEINCMS_ADMIN_MEDIA': FEINCMS_ADMIN_MEDIA,
+            'title': cl.title,
+            'is_popup': cl.is_popup,
+            'cl': cl,
+            'has_add_permission': self.has_add_permission(request),
+            'root_path': self.admin_site.root_path,
+            'app_label': app_label,
+            'object_list': self.model._tree_manager.all(),
+        }
+        context.update(extra_context or {})
+        return render_to_response([
+            'admin/feincms/%s/%s/tree_editor.html' % (app_label, opts.object_name.lower()),
+            'admin/feincms/%s/tree_editor.html' % app_label,
+            'admin/feincms/tree_editor.html',
+            ], context, context_instance=template.RequestContext(request))
+
+    def _save_tree(self, request):
+        pagetree = simplejson.loads(request.POST['tree'])
+        # 0 = tree_id, 1 = parent_id, 2 = left, 3 = right, 4 = level, 5 = item_id
+        sql = "UPDATE %s SET %s=%%s, %s_id=%%s, %s=%%s, %s=%%s, %s=%%s WHERE %s=%%s" % (
+            self.model._meta.db_table,
+            self.model._meta.tree_id_attr,
+            self.model._meta.parent_attr,
+            self.model._meta.left_attr,
+            self.model._meta.right_attr,
+            self.model._meta.level_attr,
+            self.model._meta.pk.column)
+
+        connection.cursor().executemany(sql, pagetree)
+        transaction.commit_unless_managed()
+
+        return HttpResponse("OK", mimetype="text/plain")
+
+    def _delete_item(self, request):
+        page_id = request.POST['item_id']
+        obj = self.model._default_manager.get(pk=unquote(page_id))
+        obj.delete()
+        return HttpResponse("OK", mimetype="text/plain")
+
diff --git a/feincms/content/__init__.py b/feincms/content/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/content/file/__init__.py b/feincms/content/file/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/content/file/models.py b/feincms/content/file/models.py
new file mode 100644
index 0000000..4ad4f20
--- /dev/null
+++ b/feincms/content/file/models.py
@@ -0,0 +1,20 @@
+from django.db import models
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext_lazy as _
+
+
+class FileContent(models.Model):
+    title = models.CharField(max_length=200)
+    file = models.FileField(_('file'), upload_to='filecontent')
+
+    class Meta:
+        abstract = True
+        verbose_name = _('file')
+        verbose_name_plural = _('files')
+
+    def render(self, **kwargs):
+        return render_to_string([
+            'content/file/%s.html' % self.region.key,
+            'content/file/default.html',
+            ], {'content': self})
+
diff --git a/feincms/content/image/__init__.py b/feincms/content/image/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/content/image/models.py b/feincms/content/image/models.py
new file mode 100644
index 0000000..2f1d2c4
--- /dev/null
+++ b/feincms/content/image/models.py
@@ -0,0 +1,32 @@
+from django.db import models
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext_lazy as _
+
+class ImageContent(models.Model):
+    """
+    Create an ImageContent like this:
+
+    Cls.create_content_type(ImageContent, POSITION_CHOICES=(
+        ('left', 'Left'),
+        ('right', Right'),
+        ))
+    """
+
+    image = models.ImageField(_('image'), upload_to='imagecontent')
+
+    class Meta:
+        abstract = True
+        verbose_name = _('image')
+        verbose_name_plural = _('images')
+
+    def render(self, **kwargs):
+        return render_to_string([
+            'content/image/%s.html' % self.position,
+            'content/image/default.html',
+            ], {'content': self})
+
+    @classmethod
+    def handle_kwargs(cls, POSITION_CHOICES=()):
+        models.CharField(_('position'), max_length=10, choices=POSITION_CHOICES
+            ).contribute_to_class(cls, 'position')
+
diff --git a/feincms/content/richtext/__init__.py b/feincms/content/richtext/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/content/richtext/models.py b/feincms/content/richtext/models.py
new file mode 100644
index 0000000..9d5c01c
--- /dev/null
+++ b/feincms/content/richtext/models.py
@@ -0,0 +1,16 @@
+from django.db import models
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+
+
+class RichTextContent(models.Model):
+    text = models.TextField(_('text'), blank=True)
+
+    class Meta:
+        abstract = True
+        verbose_name = _('rich text')
+        verbose_name_plural = _('rich texts')
+
+    def render(self, **kwargs):
+        return mark_safe(self.text)
+
diff --git a/feincms/content/rss/__init__.py b/feincms/content/rss/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/content/rss/models.py b/feincms/content/rss/models.py
new file mode 100644
index 0000000..8dd3aa6
--- /dev/null
+++ b/feincms/content/rss/models.py
@@ -0,0 +1,39 @@
+from datetime import datetime
+
+from django.db import models
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+from django.template.loader import render_to_string
+
+import feedparser
+
+
+class RSSContent(models.Model):
+    title = models.CharField(help_text=_('The rss field is updated several times a day. A change in the title will only be visible on the home page after the next feed update.'), max_length=50)
+    link = models.URLField(_('link'))
+    rendered_content = models.TextField(_('Pre-rendered content'), blank=True, editable=False)
+    last_updated = models.DateTimeField(_('Last updated'), blank=True, null=True)
+    max_items = models.IntegerField(_('Max. items'), default=5)
+
+    class Meta:
+        abstract = True
+        verbose_name = _('RSS feed')
+        verbose_name_plural = _('RSS feeds')
+
+    def render(self, **kwargs):
+        return mark_safe(self.rendered_content)
+
+    def cache_content(self):
+        print u"Getting RSS feed at %s" % (self.link,)
+        feed = feedparser.parse(self.link)
+
+        print u"Pre-rendering content"
+        self.rendered_content = render_to_string('content/rss/content.html', {
+            'feed_title': self.title,
+            'feed_link': feed['feed']['link'],
+            'entries': feed['entries'][:self.max_items],
+            })
+        self.last_updated = datetime.now()
+
+        self.save()
+
diff --git a/feincms/content/video/__init__.py b/feincms/content/video/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/content/video/models.py b/feincms/content/video/models.py
new file mode 100644
index 0000000..f198235
--- /dev/null
+++ b/feincms/content/video/models.py
@@ -0,0 +1,26 @@
+from django.db import models
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+import re
+
+class VideoContent(models.Model):
+    video = models.URLField(_('video link'),help_text=_('This should be a link to a youtube video, i.e.: http://www.youtube.com/watch?v=zmj1rpzDRZ0'))
+
+    class Meta:
+        abstract = True
+        verbose_name = _('video')
+        verbose_name_plural = _('videos')
+
+    def render(self, **kwargs):
+        vid = re.search('(?<==)\w+',self.video)
+        ret = """
+            <div class="videocontent">
+            <object width="400" height="330">
+            <param name="movie" value="http://www.youtube.com/v/%s&hl=de&fs=1"></param>
+                <param name="allowFullScreen" value="true"></param>
+                <param name="allowscriptaccess" value="always"></param>
+                <embed src="http://www.youtube.com/v/%s&hl=de&fs=1&rel=0" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="400" height="330"></embed>
+            </object>
+            </div>
+            """ % (vid.group(0), vid.group(0))
+        return ret
diff --git a/feincms/locale/de/LC_MESSAGES/django.po b/feincms/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 0000000..6b3ceea
--- /dev/null
+++ b/feincms/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,281 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2009-04-23 11:12+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: models.py:33 module/page/models.py:131
+msgid "title"
+msgstr "Titel"
+
+#: models.py:34
+msgid "key"
+msgstr "Schlüssel"
+
+#: models.py:35
+msgid "inherited"
+msgstr "Vererbt"
+
+#: models.py:36
+msgid ""
+"Should the content be inherited by subpages if they do not define any "
+"content for this region?"
+msgstr ""
+"Soll der Inhalt dieser Region durch Unterseiten geerbt werden, sofern diese "
+"keinen eigenen Inhalt definieren?"
+
+#: models.py:39
+msgid "region"
+msgstr "Region"
+
+#: models.py:40
+msgid "regions"
+msgstr "Regionen"
+
+#: models.py:57
+msgid "template"
+msgstr "Template"
+
+#: models.py:58
+msgid "templates"
+msgstr "Templates"
+
+#: models.py:135
+msgid "ordering"
+msgstr "Sortierung"
+
+#: admin/editor.py:139
+msgid "Database error"
+msgstr "Datenbankfehler"
+
+#: content/file/models.py:8 content/file/models.py:12
+msgid "file"
+msgstr "Datei"
+
+#: content/file/models.py:13
+msgid "files"
+msgstr "Dateien"
+
+#: content/image/models.py:15 content/image/models.py:19
+msgid "image"
+msgstr "Bild"
+
+#: content/image/models.py:20
+msgid "images"
+msgstr "Bilder"
+
+#: content/image/models.py:30
+msgid "position"
+msgstr "Position"
+
+#: content/richtext/models.py:7
+msgid "text"
+msgstr "Text"
+
+#: content/richtext/models.py:11
+msgid "rich text"
+msgstr "Text"
+
+#: content/richtext/models.py:12
+msgid "rich texts"
+msgstr "Texte"
+
+#: content/rss/models.py:12
+msgid ""
+"The rss field is updated several times a day. A change in the title will "
+"only be visible on the home page after the next feed update."
+msgstr ""
+"Der RSS Feed wird mehrmals täglich aktualisiert. Eine Änderung des Titels "
+"erscheint erst nach der nächsten Feed-Aktualisierung auf der Webseite."
+
+#: content/rss/models.py:13
+msgid "link"
+msgstr "Link"
+
+#: content/rss/models.py:14
+msgid "Pre-rendered content"
+msgstr "Vor-gerenderter Inhalt"
+
+#: content/rss/models.py:15
+msgid "Last updated"
+msgstr "Letzte Aktualisierung"
+
+#: content/rss/models.py:16
+msgid "Max. items"
+msgstr "Maximale Anzahl"
+
+#: content/rss/models.py:20
+msgid "RSS feed"
+msgstr "RSS Feed"
+
+#: content/rss/models.py:21
+msgid "RSS feeds"
+msgstr "RSS Feeds"
+
+#: content/video/models.py:7
+msgid "video link"
+msgstr "Video-Link"
+
+#: content/video/models.py:7
+msgid ""
+"This should be a link to a youtube video, i.e.: http://www.youtube.com/watch?"
+"v=zmj1rpzDRZ0"
+msgstr ""
+"Dies sollte ein Link zu einem Youtube-Video sein, z.B.: http://www.youtube."
+"com/watch?v=zmj1rpzDRZ0"
+
+#: content/video/models.py:11
+msgid "video"
+msgstr "Video"
+
+#: content/video/models.py:12
+msgid "videos"
+msgstr "Videos"
+
+#: module/page/admin.py:17
+msgid "Other options"
+msgstr "Weitere Optionen"
+
+#: module/page/models.py:36 module/page/models.py:146
+msgid "navigation extension"
+msgstr "Navigations-Erweiterung"
+
+#: module/page/models.py:128 templates/admin/feincms/tree_editor.html:133
+msgid "active"
+msgstr "Aktiv"
+
+#: module/page/models.py:132
+msgid "This is used for the generated navigation too."
+msgstr "Dies wird auch für die generierte Navigation verwendet."
+
+#: module/page/models.py:135
+msgid "in navigation"
+msgstr "In der Navigation"
+
+#: module/page/models.py:136
+msgid "override URL"
+msgstr "Überschriebene URL"
+
+#: module/page/models.py:137
+msgid "Override the target URL for the navigation."
+msgstr "Überschreibe die Ziel-URL für die Navigation."
+
+#: module/page/models.py:138
+msgid "redirect to"
+msgstr "Weiterleiten zu"
+
+#: module/page/models.py:139
+msgid "Target URL for automatic redirects."
+msgstr "Ziel-URL für automatische Weiterleitungen."
+
+#: module/page/models.py:140
+msgid "Cached URL"
+msgstr "Zwischengespeicherte URL"
+
+#: module/page/models.py:148
+msgid ""
+"Select the module providing subpages for this page if you need to customize "
+"the navigation."
+msgstr "Wähle das Modul aus, welches weitere Navigationspunkte erstellt."
+
+#: module/page/models.py:151
+msgid "content title"
+msgstr "Inhaltstitel"
+
+#: module/page/models.py:152
+msgid "The first line is the main title, the following lines are subtitles."
+msgstr "Die erste Zeile ist der Haupttitel, die weiteren Zeilen Untertitel"
+
+#: module/page/models.py:155
+msgid "page title"
+msgstr "Seitentitel"
+
+#: module/page/models.py:156
+msgid "Page title for browser window. Same as title by default."
+msgstr ""
+"Seitentitel für das Browser-Fenster. Standardmässig gleich wie der Titel."
+
+#: module/page/models.py:157
+msgid "meta keywords"
+msgstr "Meta Begriffe"
+
+#: module/page/models.py:158
+msgid "This will be prepended to the default keyword list."
+msgstr "Diese Begriffe werden vor die Standard-Begriffsliste eingefügt."
+
+#: module/page/models.py:159
+msgid "meta description"
+msgstr "Meta Beschreibung"
+
+#: module/page/models.py:160
+msgid "This will be prepended to the default description."
+msgstr "Diese Beschreibung wird vor der Standard-Beschreibung eingefügt."
+
+#: module/page/models.py:163
+msgid "language"
+msgstr "Sprache"
+
+#: module/page/models.py:169
+msgid "page"
+msgstr "Seite"
+
+#: module/page/models.py:170
+msgid "pages"
+msgstr "Seiten"
+
+#: templates/admin/feincms/item_editor.html:122
+msgid "Home"
+msgstr "Startseite"
+
+#: templates/admin/feincms/item_editor.html:134
+msgid "Delete"
+msgstr "Löschen"
+
+#: templates/admin/feincms/item_editor.html:139
+msgid "Save"
+msgstr "Speichern"
+
+#: templates/admin/feincms/item_editor.html:143
+msgid "Change Template"
+msgstr "Template ändern"
+
+#: templates/admin/feincms/item_editor.html:158
+msgid "Region empty"
+msgstr "Region leer"
+
+#: templates/admin/feincms/item_editor.html:162
+msgid ""
+"Content from the parent site is automatically inherited. To override this "
+"behaviour, add some content."
+msgstr ""
+"Inhalt wird von der übergeordneten Seite geerbt. Füge Inhalt hinzu, um "
+"dieses Verhalten zu ändern"
+
+#: templates/admin/feincms/tree_editor.html:121
+#, python-format
+msgid "Add %(name)s"
+msgstr "%(name)s hinzufügen"
+
+#: templates/admin/feincms/tree_editor.html:132
+msgid "Page"
+msgstr "Seite"
+
+#: templates/admin/feincms/tree_editor.html:134
+msgid "in navi"
+msgstr "Im Menü"
+
+#: templates/admin/feincms/tree_editor.html:135
+msgid "delete"
+msgstr "Löschen"
diff --git a/feincms/management/__init__.py b/feincms/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/management/commands/__init__.py b/feincms/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/management/commands/update_rsscontent.py b/feincms/management/commands/update_rsscontent.py
new file mode 100644
index 0000000..ee3189f
--- /dev/null
+++ b/feincms/management/commands/update_rsscontent.py
@@ -0,0 +1,12 @@
+from django.core.management.base import NoArgsCommand
+
+from feincms.content.rss.models import RSSContent
+
+class Command(NoArgsCommand):
+    help = "Run this as a cronjob."
+
+    def handle_noargs(self, **options):
+        for cls in RSSContent._feincms_content_models:
+            for content in cls.objects.all():
+                content.cache_content()
+
diff --git a/feincms/models.py b/feincms/models.py
new file mode 100644
index 0000000..8963161
--- /dev/null
+++ b/feincms/models.py
@@ -0,0 +1,224 @@
+import copy
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.db import models
+from django.db.models import Q
+from django.http import Http404
+from django.utils import translation
+from django.utils.translation import ugettext_lazy as _
+
+import mptt
+
+
+class TypeRegistryMetaClass(type):
+    """
+    You can access the list of subclasses as <BaseClass>.types
+    """
+
+    def __init__(cls, name, bases, attrs):
+        if not hasattr(cls, 'types'):
+            cls.types = []
+        else:
+            cls.types.append(cls)
+
+
+class Region(models.Model):
+    """
+    A template region which will be a container for several page contents.
+
+    Often used regions might be "main" and "sidebar"
+    """
+
+    title = models.CharField(_('title'), max_length=50, unique=True)
+    key = models.CharField(_('key'), max_length=20, unique=True)
+    inherited = models.BooleanField(_('inherited'), default=False,
+        help_text=_('Should the content be inherited by subpages if they do not define any content for this region?'))
+
+    class Meta:
+        verbose_name = _('region')
+        verbose_name_plural = _('regions')
+
+    def __unicode__(self):
+        return self.title
+
+
+class Template(models.Model):
+    """
+    A template file on the disk which can be used by pages to render themselves.
+    """
+
+    title = models.CharField(max_length=200)
+    path = models.CharField(max_length=200)
+    regions = models.ManyToManyField(Region, related_name='templates')
+
+    class Meta:
+        ordering = ['title']
+        verbose_name = _('template')
+        verbose_name_plural = _('templates')
+
+    def __unicode__(self):
+        return self.title
+
+
+def first_template():
+    return Template.objects.all()[0]
+
+
+class Base(models.Model):
+    template = models.ForeignKey(Template, default=first_template)
+
+    class Meta:
+        abstract = True
+
+    @property
+    def content(self):
+        if not hasattr(self, '_content_proxy'):
+            self._content_proxy = ContentProxy(self)
+
+        return self._content_proxy
+
+    def _content_for_region(self, region):
+        if not hasattr(self, '_feincms_content_types'):
+            raise ImproperlyConfigured, 'You need to create at least one content type for the %s model.' % (self.__class__.__name__)
+
+        sql = ' UNION '.join([
+            'SELECT %d, COUNT(id) FROM %s WHERE parent_id=%s AND region_id=%s' % (
+                idx,
+                cls._meta.db_table,
+                self.pk,
+                region.id) for idx, cls in enumerate(self._feincms_content_types)])
+
+        from django.db import connection
+        cursor = connection.cursor()
+        cursor.execute(sql)
+
+        counts = [row[1] for row in cursor.fetchall()]
+
+        if not any(counts):
+            return []
+
+        contents = []
+        for idx, cnt in enumerate(counts):
+            if cnt:
+                contents += list(
+                    self._feincms_content_types[idx].objects.filter(
+                        parent=self,
+                        region=region).select_related('parent', 'region'))
+
+        return contents
+
+    @classmethod
+    def _create_content_base(cls):
+        class Meta:
+            abstract = True
+            ordering = ['ordering']
+
+        def __unicode__(self):
+            return u'%s on %s, ordering %s' % (self.region, self.parent, self.ordering)
+
+        def render(self, **kwargs):
+            render_fn = getattr(self, 'render_%s' % self.region.key, None)
+
+            if render_fn:
+                return render_fn(**kwargs)
+
+            raise NotImplementedError
+
+        attrs = {
+            '__module__': cls.__module__,
+            '__unicode__': __unicode__,
+            'render': render,
+            'Meta': Meta,
+            'parent': models.ForeignKey(cls, related_name='%(class)s_set'),
+            'region': models.ForeignKey(Region, related_name='%s_%%(class)s_set' % cls.__name__.lower()),
+            'ordering': models.IntegerField(_('ordering'), default=0),
+            }
+
+        cls._feincms_content_model = type('%sContent' % cls.__name__,
+            (models.Model,), attrs)
+
+        cls._feincms_content_types = []
+
+        return cls._feincms_content_model
+
+    @classmethod
+    def create_content_type(cls, model, **kwargs):
+        if not hasattr(cls, '_feincms_content_model'):
+            cls._create_content_base()
+
+        feincms_content_base = getattr(cls, '_feincms_content_model')
+
+        class Meta:
+            db_table = '%s_%s' % (cls._meta.db_table, model.__name__.lower())
+            verbose_name = model._meta.verbose_name
+            verbose_name_plural = model._meta.verbose_name_plural
+
+        attrs = {
+            '__module__': cls.__module__,
+            'Meta': Meta,
+            }
+
+        new_type = type(
+            model.__name__,
+            (model, feincms_content_base,), attrs)
+        cls._feincms_content_types.append(new_type)
+
+        if not hasattr(model, '_feincms_content_models'):
+            model._feincms_content_models = []
+
+        model._feincms_content_models.append(new_type)
+
+        if hasattr(new_type, 'handle_kwargs'):
+            new_type.handle_kwargs(**kwargs)
+        else:
+            for k, v in kwargs.items():
+                setattr(new_type, k, v)
+
+        return new_type
+
+
+class ContentProxy(object):
+    """
+    This proxy offers attribute-style access to the page contents of regions.
+
+    Example:
+    >>> page = Page.objects.all()[0]
+    >>> page.content.main
+    [A list of all page contents which are assigned to the region with key 'main']
+    """
+
+    def __init__(self, item):
+        self.item = item
+
+    def __getattr__(self, attr):
+        """
+        Get all item content instances for the specified item and region
+
+        If no item contents could be found for the current item and the region
+        has the inherited flag set, this method will go up the ancestor chain
+        until either some item contents have found or no ancestors are left.
+        """
+
+        item = self.__dict__['item']
+
+        try:
+            region = item.template.regions.get(key=attr)
+        except Region.DoesNotExist:
+            return []
+
+        def collect_items(obj):
+            contents = obj._content_for_region(region)
+
+            # go to parent if this model has a parent attribute
+            # TODO this should be abstracted into a property/method or something
+            # The link which should be followed is not always '.parent'
+            if not contents and hasattr(obj, 'parent_id') and obj.parent_id and region.inherited:
+                return collect_items(obj.parent)
+
+            return contents
+
+        contents = collect_items(item)
+        contents.sort(key=lambda c: c.ordering)
+        return contents
+
diff --git a/feincms/module/__init__.py b/feincms/module/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/module/page/__init__.py b/feincms/module/page/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/module/page/admin.py b/feincms/module/page/admin.py
new file mode 100644
index 0000000..486605f
--- /dev/null
+++ b/feincms/module/page/admin.py
@@ -0,0 +1,33 @@
+from django.contrib import admin
+from django.utils.translation import ugettext_lazy as _
+
+
+from feincms.admin import editor
+from feincms.module.page.models import Page
+
+
+class PageAdmin(editor.ItemEditorMixin, editor.TreeEditorMixin, admin.ModelAdmin):
+    # the fieldsets config here is used for the add_view, it has no effect
+    # for the change_view which is completely customized anyway
+    fieldsets = (
+        (None, {
+            'fields': ('active', 'in_navigation', 'template', 'title', 'slug',
+                'parent', 'language'),
+        }),
+        (_('Other options'), {
+            'classes': ('collapse',),
+            'fields': ('override_url', 'meta_keywords', 'meta_description'),
+        }),
+        )
+    list_display=('__unicode__', 'active', 'in_navigation',
+        'language', 'template')
+    list_filter=('active', 'in_navigation', 'language', 'template')
+    search_fields = ('title', 'slug', '_content_title', '_page_title',
+        'meta_keywords', 'meta_description')
+    prepopulated_fields={
+        'slug': ('title',),
+        }
+
+    show_on_top = ('title', 'active', 'in_navigation')
+
+admin.site.register(Page, PageAdmin)
diff --git a/feincms/module/page/models.py b/feincms/module/page/models.py
new file mode 100644
index 0000000..623ab88
--- /dev/null
+++ b/feincms/module/page/models.py
@@ -0,0 +1,232 @@
+from django.conf import settings
+from django.db import models
+from django.db.models import Q
+from django.http import Http404
+from django.utils import translation
+from django.utils.translation import ugettext_lazy as _
+
+import mptt
+
+from feincms.models import TypeRegistryMetaClass, Region, Template,\
+    Base, ContentProxy
+
+
+def get_object(path, fail_silently=False):
+    dot = path.rindex('.')
+    try:
+        return getattr(__import__(path[:dot], {}, {}, ['']), path[dot+1:])
+    except ImportError:
+        if not fail_silently:
+            raise
+
+    return None
+
+
+class PagePretender(object):
+    def __init__(self, **kwargs):
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+    def get_absolute_url(self):
+        return self.url
+
+
+class NavigationExtension(object):
+    __metaclass__ = TypeRegistryMetaClass
+    name = _('navigation extension')
+
+    def children(self, page, **kwargs):
+        raise NotImplementedError
+
+
+class PageManager(models.Manager):
+    def active(self):
+        return self.filter(active=True)
+
+    def page_for_path(self, path, raise404=False):
+        """
+        Return a page for a path.
+
+        Example:
+        Page.objects.page_for_path(request.path)
+        """
+
+        stripped = path.strip('/')
+
+        try:
+            return self.active().filter(override_url='/%s/' % stripped)[0]
+        except IndexError:
+            pass
+
+        tokens = stripped.split('/')
+
+        count = len(tokens)
+
+        filters = {'%sisnull' % ('parent__' * count): True}
+
+        for n, token in enumerate(tokens):
+            filters['%sslug' % ('parent__' * (count-n-1))] = token
+
+        try:
+            return self.active().filter(**filters)[0]
+        except IndexError:
+            if raise404:
+                raise Http404
+            raise self.model.DoesNotExist
+
+    def page_for_path_or_404(self, path):
+        """
+        Wrapper for page_for_path which raises a Http404 if no page
+        has been found for the passed path.
+        """
+        return self.page_for_path(path, raise404=True)
+
+    def best_match_for_path(self, path, raise404=False):
+        """
+        Return the best match for a path.
+        """
+
+        tokens = path.strip('/').split('/')
+
+        for count in range(len(tokens), -1, -1):
+            try:
+                return self.page_for_path('/'.join(tokens[:count]))
+            except self.model.DoesNotExist:
+                pass
+
+        if raise404:
+            raise Http404
+        return None
+
+    def in_navigation(self):
+        return self.active().filter(in_navigation=True)
+
+    def toplevel_navigation(self):
+        return self.in_navigation().filter(parent__isnull=True)
+
+    def for_request(self, request, raise404=False):
+        page = self.page_for_path(request.path, raise404)
+        page.setup_request(request)
+        return page
+
+    def for_request_or_404(self, request):
+        return self.page_for_path_or_404(request.path, raise404=True)
+
+    def best_match_for_request(self, request, raise404=False):
+        page = self.best_match_for_path(request.path, raise404)
+        page.setup_request(request)
+        return page
+
+    def from_request(self, request):
+        if hasattr(request, '_feincms_page'):
+            return request._feincms_page
+
+        return self.for_request(request)
+
+
+class Page(Base):
+    active = models.BooleanField(_('active'), default=False)
+
+    # structure and navigation
+    title = models.CharField(_('title'), max_length=100,
+        help_text=_('This is used for the generated navigation too.'))
+    slug = models.SlugField()
+    parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
+    in_navigation = models.BooleanField(_('in navigation'), default=True)
+    override_url = models.CharField(_('override URL'), max_length=200, blank=True,
+        help_text=_('Override the target URL for the navigation.'))
+    redirect_to = models.CharField(_('redirect to'), max_length=200, blank=True,
+        help_text=_('Target URL for automatic redirects.'))
+    _cached_url = models.CharField(_('Cached URL'), max_length=200, blank=True,
+        editable=False, default='')
+
+    # navigation extensions
+    NE_CHOICES = [(
+        '%s.%s' % (cls.__module__, cls.__name__), cls.name) for cls in NavigationExtension.types]
+    navigation_extension = models.CharField(_('navigation extension'),
+        choices=NE_CHOICES, blank=True, max_length=50,
+        help_text=_('Select the module providing subpages for this page if you need to customize the navigation.'))
+
+    # content
+    _content_title = models.TextField(_('content title'), blank=True,
+        help_text=_('The first line is the main title, the following lines are subtitles.'))
+
+    # meta stuff TODO keywords and description?
+    _page_title = models.CharField(_('page title'), max_length=100, blank=True,
+        help_text=_('Page title for browser window. Same as title by default.'))
+    meta_keywords = models.TextField(_('meta keywords'), blank=True,
+        help_text=_('This will be prepended to the default keyword list.'))
+    meta_description = models.TextField(_('meta description'), blank=True,
+        help_text=_('This will be prepended to the default description.'))
+
+    # language
+    language = models.CharField(_('language'), max_length=10,
+        choices=settings.LANGUAGES)
+    translations = models.ManyToManyField('self', blank=True)
+
+    class Meta:
+        ordering = ['tree_id', 'lft']
+        verbose_name = _('page')
+        verbose_name_plural = _('pages')
+
+    objects = PageManager()
+
+    def __unicode__(self):
+        return u'%s (%s)' % (self.title, self.get_absolute_url())
+
+    def save(self, *args, **kwargs):
+        super(Page, self).save(*args, **kwargs)
+        pages = self.get_descendants(include_self=True)
+        for page in pages:
+            page._generate_cached_url()
+
+    def _generate_cached_url(self):
+        if self.override_url:
+            self._cached_url = self.override_url
+        if self.is_root_node():
+            self._cached_url = u'/%s/' % (self.slug)
+        else:
+            self._cached_url = u'/%s/%s/' % ('/'.join([page.slug for page in self.get_ancestors()]), self.slug)
+
+        super(Page, self).save()
+
+    def get_absolute_url(self):
+        return self._cached_url
+
+    @property
+    def page_title(self):
+        if self._page_title:
+            return self._page_title
+        return self.content_title
+
+    @property
+    def content_title(self):
+        if not self._content_title:
+            return self.title
+
+        try:
+            return self._content_title.splitlines()[0]
+        except IndexError:
+            return u''
+
+    @property
+    def content_subtitle(self):
+        return u'\n'.join(self._content_title.splitlines()[1:])
+
+    def setup_request(self, request):
+        translation.activate(self.language)
+        request.LANGUAGE_CODE = translation.get_language()
+        request._feincms_page = self
+
+    def extended_navigation(self):
+        if not self.navigation_extension:
+            return []
+
+        cls = get_object(self.navigation_extension, fail_silently=True)
+        if not cls:
+            return []
+
+        return cls().children(self)
+
+mptt.register(Page)
+
diff --git a/feincms/module/page/templatetags/__init__.py b/feincms/module/page/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/module/page/templatetags/feincms_page_tags.py b/feincms/module/page/templatetags/feincms_page_tags.py
new file mode 100644
index 0000000..68b2ee3
--- /dev/null
+++ b/feincms/module/page/templatetags/feincms_page_tags.py
@@ -0,0 +1,63 @@
+from django import template
+from feincms.module.page.models import Page
+from feincms.templatetags.utils import *
+
+register = template.Library()
+
+
+class NavigationNode(SimpleAssignmentNodeWithVarAndArgs):
+    """
+    Example:
+    {% feincms_navigation of feincms_page as sublevel level=2 %}
+    {% for p in sublevel %}
+        <a href="{{ p.get_absolute_url }}">{{ p.title }}</a>
+    {% endfor %}
+    """
+
+    def what(self, instance, args):
+        level = int(args.get('level', 1))
+
+        if level <= 1:
+            return Page.objects.toplevel_navigation()
+
+        # mptt starts counting at 0, NavigationNode at 1; if we need the submenu
+        # of the current page, we have to add 2 to the mptt level
+        if instance.level+2 == level:
+            return instance.children.in_navigation()
+
+        try:
+            return instance.get_ancestors()[level-2].children.in_navigation()
+        except IndexError:
+            return []
+register.tag('feincms_navigation', do_simple_assignment_node_with_var_and_args_helper(NavigationNode))
+
+
+class ParentLinkNode(SimpleNodeWithVarAndArgs):
+    """
+    {% feincms_parentlink of feincms_page level=1 %}
+    """
+
+    def what(self, page, args):
+        level = int(args.get('level', 1))
+
+        if page.level+1 == level:
+            return page.get_absolute_url()
+        elif page.level+1 < level:
+            return '#'
+
+        try:
+            return page.get_ancestors()[level-1].get_absolute_url()
+        except IndexError:
+            return '#'
+register.tag('feincms_parentlink', do_simple_node_with_var_and_args_helper(ParentLinkNode))
+
+
+class BestMatchNode(SimpleAssignmentNodeWithVar):
+    """
+    {% feincms_bestmatch for request.path as feincms_page %}
+    """
+
+    def what(self, path):
+        return Page.objects.best_match_for_path(path)
+register.tag('feincms_bestmatch', do_simple_assignment_node_with_var_helper(BestMatchNode))
+
diff --git a/feincms/shortcuts.py b/feincms/shortcuts.py
new file mode 100644
index 0000000..a0e4e19
--- /dev/null
+++ b/feincms/shortcuts.py
@@ -0,0 +1,13 @@
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from feincms.module.page.models import Page
+
+
+def render_to_response_best_match(request, template_name, dictionary=None):
+    dictionary = dictionary or {}
+    dictionary['feincms_page'] = Page.objects.best_match_for_request(request)
+
+    return render_to_response(template_name, dictionary,
+        context_instance=RequestContext(request))
+
diff --git a/feincms/templates/admin/feincms/item_editor.html b/feincms/templates/admin/feincms/item_editor.html
new file mode 100644
index 0000000..c61b8c2
--- /dev/null
+++ b/feincms/templates/admin/feincms/item_editor.html
@@ -0,0 +1,227 @@
+{% extends "admin/change_form.html" %}
+{% load i18n admin_modify adminmedia %}
+
+{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
+{% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %}
+
+{% block extrahead %}{{ block.super }}
+<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />
+
+<script type="text/javascript" src="../../../jsi18n/"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}jquery-1.3.min.js"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}jquery.ui.all.js"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}jquery.livequery.js"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}jquery.alerts.js"></script>
+
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}helper.js"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}listener.js"></script>
+
+<script type="text/javascript" src="/media/sys/feinheit/tinymce/tiny_mce.js"></script>
+
+<script type="text/javascript">
+
+    tinyMCE.init({
+        mode: "none",
+        theme: "advanced",
+        language: "en",
+        theme_advanced_toolbar_location: "top",
+        theme_advanced_toolbar_align: "left",
+        theme_advanced_statusbar_location: "bottom",
+        theme_advanced_buttons1: "fullscreen,|,formatselect,image,media,code,|,cut,copy,paste,|,bold,italic,|,bullist,numlist,|,link,unlink",
+        theme_advanced_buttons2: "",
+        theme_advanced_buttons3: "",
+        theme_advanced_path: false,
+        theme_advanced_blockformats: "p,h2,h3",
+        theme_advanced_resizing: true,
+        width: '600',
+        height: '300',
+        content_css: "/path_to_your_media/css/preview.css",
+        plugins: "advimage,advlink,fullscreen,table,preview,media,inlinepopups",
+        advimage_update_dimensions_onchange: true,
+        //file_browser_callback: "CustomFileBrowser",
+        relative_urls: false
+    });
+
+    function init_pagecontent() {
+        // handle special page content type needs
+        // this is not really extensible, but it works for now
+        $('.order-machine textarea[name*=richtext]:visible').each(function(){
+            tinyMCE.execCommand('mceAddControl', true, this.id);
+        });
+    }
+
+    IMG_ARROW_DOWN_PATH = "{{ FEINCMS_ADMIN_MEDIA }}img/arrow_down.gif";
+    IMG_ARROW_RIGHT_PATH = "{{ FEINCMS_ADMIN_MEDIA }}img/arrow_right.gif";
+    IMG_CIRCLE_PATH = "{{ FEINCMS_ADMIN_MEDIA }}img/circle.gif";
+    IMG_DELETELINK_PATH = "{{ FEINCMS_ADMIN_MEDIA }}img/icon_deletelink.gif";
+    IMG_MOVE_PATH = "{{ FEINCMS_ADMIN_MEDIA }}img/icon_move.gif";
+
+    REGIONS = [];
+    REGION_MAP = [];
+    {% for region in page.template.regions.all %}
+        REGIONS.push('{{ region.key }}');
+        REGION_MAP.push('{{ region.id }}');
+    {% endfor %}
+    ACTIVE_REGION = 0;
+
+    CONTENT_NAMES = {
+        {% for name, value in content_types %}'{{ value }}': '{{ name }}'{% if not forloop.last %},{% endif %}
+        {% endfor %}};
+
+    $(document).ready(function(){
+        // move contents into their corresponding regions and do some simple formatting
+        $("div[id$=_set]").children().each(function(){
+            if (!($(this).hasClass("header"))) {
+                $(this).find("select[name$=region]").addClass("region-choice-field").parents("tr").hide();
+                $(this).find("input[name$=DELETE]").addClass("delete-field").parents("tr").hide();
+                $(this).find("input[name$=ordering]").addClass("order-field").parents("tr").hide();
+                $(this).find("input[name$=id]").hide().prev().hide();
+                $(this).find("input[name$=parent]").hide().prev().hide();
+
+                var region_id = $(this).find(".region-choice-field").val();
+                region_id = REGION_MAP.indexOf(region_id);
+                var content_type = $(this).attr("id").substr(0, $(this).attr("id").indexOf("_"));
+                region_append(region_id,$(this), CONTENT_NAMES[content_type]);
+                set_item_field_value($(this),"region-choice-field",region_id)
+            }
+        });
+        // register regions as sortable for drag N drop
+        $(".order-machine").sortable({
+            handle: '.handle',
+            helper: 'clone',
+            stop: function(event, ui) {
+                richify_poor($(ui.item));
+            }
+        });
+        // hide content on drag n drop
+        $(".handle").mousedown(function(){
+            poorify_rich($(this).parents(".order-item"));
+        });
+        $(".handle").mouseup(function(){
+            richify_poor($(this).parents(".order-item"));
+        });
+        // convert text areas to rich text editors.
+        init_pagecontent();
+
+        if(window.location.hash) {
+            $(window.location.hash+'_tab').trigger('click');
+        }
+
+        // bring order to chaos
+        zucht_und_ordnung(true);
+
+        {% block extra-init-js %}{% endblock %}
+    });
+</script>
+
+<link rel="stylesheet" type="text/css" href="{{ FEINCMS_ADMIN_MEDIA }}css/layout.css" />
+<link rel="stylesheet" type="text/css" href="{{ FEINCMS_ADMIN_MEDIA }}css/jquery.alerts.css" media="screen" />
+
+{% endblock %}
+
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+     <a href="../../../">{% trans "Home" %}</a> &rsaquo;
+     <a href="../../">{{ opts.app_label|capfirst|escape }}</a> &rsaquo;
+     <a href="../">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
+     {{ page.title|truncatewords:"18" }}
+</div>
+{% endblock %}
+
+{% block content %}
+
+<div id="content-main">
+
+{% block object-tools %}
+<ul class="object-tools">
+    {% if page.get_absolute_url %}
+        <li><a target="_blank" href="{{ page.get_absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>
+    {% endif %}
+</ul>
+{% endblock %}
+
+<hr/>
+
+<form name="main_form" enctype="multipart/form-data" action="." method="post" id="{{ opts.module_name }}_form">
+
+<div id="overview">
+    <p style="float:right;" class="deletelink-box"><a href="delete/" class="deletelink">{% trans "Delete" %}</a></p>
+    {% for field in top_fieldset %}
+        {{ field.label_tag }}
+        <span>{{ field }}{{ field.errors }}</span>
+    {% endfor %}
+    <input type="submit" class="submit_form" value="{% trans 'Save' %}" />
+    <hr/>
+    {{ page_form.template.label_tag }}
+    <span>{{ page_form.template }}{{ page_form.template.errors }}</span>
+    <input type="button" class="cancel" value="{% trans 'Change Template' %}" />
+    <hr/>
+</div>
+<div id="main_wrapper">
+    <div class="navi_tab tab_active" id="settings_tab">Settings</div>
+    {% for region in page.template.regions.all %}<div class="navi_tab tab_inactive" id="{{ region.key }}_tab">{{ region.title }}</div>{% endfor %}
+    <div id="main">
+        <div id="settings_body">
+            <table>
+                {{ settings_fieldset.as_table }}
+            </table>
+        </div>
+        {% for region in page.template.regions.all %}
+        <div id="{{ region.key }}_body" class="panel">
+            <div class="empty-machine-msg">
+                {% trans "Region empty" %}
+            </div>
+            <div class="empty-machine-msg" style="margin-left:20px; margin-top:20px;">
+                {% if region.inherited %}
+                    {% trans "Content from the parent site is automatically inherited. To override this behaviour, add some content." %}
+                {% endif %}
+            </div>
+            <div class="order-machine">
+
+            </div>
+
+            <div class="machine-control">
+                <div class="control-unit">
+                    <span>Add New item:</span> <br/>
+                    <select name="order-machine-add-select">
+                    {% for n,v  in content_types %} <option value="{{ v }}">{{ n }}</option> {% endfor %}
+                    </select>
+                    <input type="button" class="order-machine-add-button button" value="OK" />
+                </div>
+                <div class="control-unit">
+                    <span>Move selected item to:</span>  <br/>
+                    <select name="order-machine-move-select">
+                    {% for r in page.template.regions.all %} {% ifnotequal region r %} <option value="{{ r.key }}">{{ r.title }}</option> {% endifnotequal %} {% endfor %}
+                    </select>
+                    <input type="button" class="order-machine-move-button button" value="OK" />
+                </div>
+            </div>
+         </div>
+        {% endfor %}
+    </div>
+</div>
+
+
+<div id="inlines" style="display:none">
+{% for formset in inline_formsets %}
+    <div id="{{ formset.rel_name }}">
+        <div class="header">
+            {{ formset.management_form }}
+            <h3>{{ formset.rel_name }}</h3>
+        </div>
+        {% for form in formset.forms %}
+            <div id="{{ formset.rel_name }}_item_{{ forloop.counter0 }}">
+            <table>
+            {{ form.as_table }}
+            </table>
+            </div>
+        {% endfor %}
+    </div>
+{% endfor %}
+</div>
+
+</form>
+
+</div>
+{% endblock %}
+
diff --git a/feincms/templates/admin/feincms/tree_editor.html b/feincms/templates/admin/feincms/tree_editor.html
new file mode 100644
index 0000000..bbde798
--- /dev/null
+++ b/feincms/templates/admin/feincms/tree_editor.html
@@ -0,0 +1,145 @@
+{% extends "admin/change_list.html" %}
+{% load i18n admin_modify adminmedia mptt_tags %}
+
+{% block title %}{{ block.super }}{% endblock %}
+
+{% block extrahead %}{{ block.super }}
+<script type="text/javascript" src="../../../jsi18n/"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}jquery-1.3.min.js"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}jquery.ui.all.js"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}jquery.livequery.js"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}jquery.alerts.js"></script>
+
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}helper.js"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}listener.js"></script>
+
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}jquery.treeTable.js"></script>
+<script type="text/javascript" src="{{ FEINCMS_ADMIN_MEDIA }}jquery.json-1.3.js"></script>
+<script type="text/javascript">
+
+
+
+    ancestors = [{% for page in object_list %}'{{ page.parent_id|default_if_none:"0" }}'{% if not forloop.last %},{% endif %} {% endfor %}];
+
+    tablestr = '';
+    {% for page in object_list %}tablestr += add_row({{ forloop.counter }}, {{ page.id }}, "{{ page.parent_id|default_if_none:"-1" }}", "{{ page.title }}", ["{{ page.active }}", "{{ page.in_navigation }}"]);
+    {% endfor %}
+
+    function add_row(node_id, page_id, parent_id, page_title, attrs) {
+        var str = '<tr id="node-' + node_id + '" class="page-id-' + page_id + ' ';
+        if (parseInt(parent_id) >= 0)
+            str += 'child-of-node-'+ancestors.indexOf(parent_id);
+        str += '">';
+        str += '<td><div class="wrap nohover">';
+        str += '<div class="insert-as-child"></div>';
+        str += '<span class="title-col"><a href="'+page_id+'"><strong>'+page_title+'</strong></a><img class="move-node" src="{{ FEINCMS_ADMIN_MEDIA }}img/icon_move.gif" /></span>';
+        str += '<div class="insert-as-sibling"></div>';
+        str += '</div></td>';
+        for (key in attrs)
+            str += '<td>'+attrs[key]+'</td>';
+        str += '<td><img class="del-page" src="{{ FEINCMS_ADMIN_MEDIA }}img/icon_deletelink.gif"/></td></tr>';
+        return str;
+    }
+
+    $(document).ready(function()  {
+            // build table
+            $("#sitetree tbody").append(tablestr);
+            // register
+            $("#sitetree").treeTable();
+            // configure draggable
+            $("#sitetree .title-col").draggable({
+              helper:  function(){ return $(this).parent().clone(); } ,
+              handle: ".move-node",
+              opacity: .75,
+              refreshPositions: true,
+              revert: "invalid",
+              revertDuration: 300,
+              scroll: true
+            });
+            // configure droppable to insert as child
+            $("#sitetree .insert-as-child").each(function() {
+              $(this).droppable({
+                accept: ".title-col",
+                tolerance: "intersect",
+                drop: function(e, ui) {
+                    handle_drop_event($(ui.draggable).parents("tr"), $(this).parents("tr"), "child")
+                },
+                over: function(e, ui) {
+                  $(this).parent().removeClass("nohover").addClass("hover-as-child");
+                },
+                out: function(e, ui) {
+                  $(this).parent().removeClass("hover-as-child").addClass("nohover");
+                }
+              });
+            });
+            // configure droppable to insert as sibling
+            $("#sitetree .insert-as-sibling").each(function() {
+              $(this).droppable({
+                accept: ".title-col",
+                tolerance: "intersect",
+                drop: function(e, ui) {
+                    handle_drop_event($(ui.draggable).parents("tr"), $(this).parents("tr"), "sibling")
+                },
+                over: function(e, ui) {
+                  var row = '<div style="background-color:#bcf; height:4px; width:100%; margin:-8px 0px 4px -5px; position:relative; z-index:10;"></div>'
+                  $(row).insertBefore($(this).parent());
+                },
+                out: function(e, ui) {
+                  $(this).parent().prev().remove();
+                }
+              });
+            });
+
+            $(".wrap").live('click',function() {
+                if ($(this).find(".expander").length > 0)
+                    $(this).parents("tr").toggleBranch();
+            });
+
+            $(".save_tree").click(function(){
+                    save_page_tree();
+            });
+
+            $(".del-page").click(function(){
+                handle_page_delete($(this).parents("tr"));
+            });
+
+    });
+
+</script>
+
+<link rel="stylesheet" type="text/css" href="{{ FEINCMS_ADMIN_MEDIA }}css/layout.css" />
+<link rel="stylesheet" type="text/css" href="{{ FEINCMS_ADMIN_MEDIA }}css/jquery.alerts.css" media="screen" />
+<link href="{{ FEINCMS_ADMIN_MEDIA }}css/jquery.treeTable.css" rel="stylesheet" type="text/css" />
+
+{% endblock %}
+
+{% block content %}
+
+<div id="content-main">
+    {% block object-tools %}
+    {% if has_add_permission %}
+    <ul class="object-tools"><li><a href="add/{% if is_popup %}?_popup=1{% endif %}" class="addlink">{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}</a></li></ul>
+    {% endif %}
+    {% endblock %}
+</div>
+
+<input type="button" value="save tree" class="save_tree" style="margin: 20px 5px -10px 460px;"/>
+
+<div id="sitetree-wrapper">
+<table id="sitetree" border="1">
+    <thead>
+    <tr id="table_header">
+        <th width="400">{% trans  "Page" %}</th>
+        <th>{% trans  "active" %}</th>
+        <th>{% trans  "in navi" %}</th>
+        <th>{% trans  "delete" %}</th>
+    </tr>
+    </thead>
+    <tbody>
+
+    </tbody>
+</table>
+</div>
+
+{% endblock %}
+
diff --git a/feincms/templates/content/rss/content.html b/feincms/templates/content/rss/content.html
new file mode 100644
index 0000000..c1eaaf9
--- /dev/null
+++ b/feincms/templates/content/rss/content.html
@@ -0,0 +1,8 @@
+<h2><a href="{{ feed_link }}">{{ feed_title }}</a></h2>
+
+<ul>
+{% for entry in entries %}
+    <li><a href="{{ entry.link }}">{{ entry.title }}</a></li>
+{% endfor %}
+</ul>
+
diff --git a/feincms/templatetags/__init__.py b/feincms/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/templatetags/feincms_tags.py b/feincms/templatetags/feincms_tags.py
new file mode 100644
index 0000000..da03d9f
--- /dev/null
+++ b/feincms/templatetags/feincms_tags.py
@@ -0,0 +1,75 @@
+from django import template
+from feincms.module.page.models import Page
+
+register = template.Library()
+
+
+@register.simple_tag
+def feincms_render_region(page, region, request):
+    """
+    {% feincms_render_region feincms_page "main" request %}
+    """
+
+    contents = getattr(page.content, region)
+
+    return u''.join(content.render(request=request) for content in contents)
+
+
+@register.simple_tag
+def feincms_render_content(content, request):
+    """
+    {% feincms_render_content pagecontent request %}
+    """
+
+    return content.render(request=request)
+
+
+class NaviLevelNode(template.Node):
+    """ Gets navigation based on current page OR request, dependant on choice of second parameter (of vs. from).
+
+        Top navigation level is 1.
+        If navigation level + 1 > page.level, the ouput is none, because there is no well-defined sub-sub-navigation for a page.
+
+        Example usage:
+        1) {% feincms_get_navi_level 1 of page as pages %}
+        2) {% feincms_get_navi_level 1 from request as pages %}
+
+        Side-note:  If not using mptt to retrieve pages, the ordering cannot be dertermined by 'id'.
+        Instead, as a "hack", we can sort by field 'lft', because we understand how mptt works :-)
+    """
+    def __init__(self, level, switch, obj, dummy, varname):
+        self.level = long(int(level) - 1)
+        self.obj = template.Variable(obj)
+        self.varname = varname
+        self.switch = switch
+
+    def render(self, context):
+        if self.switch == 'of':
+            # obj is a Page
+            page = self.obj.resolve(context)
+        else: # self.switch == 'from'
+            # obj is a request
+            page = Page.objects.from_request(self.obj.resolve(context))
+
+        if int(self.level) == 0:
+            # top level
+            pages = Page.objects.filter(in_navigation=True, level=long(0)).order_by('lft')
+        elif self.level <= page.level:
+            ancestor = page.get_ancestors()[int(self.level) - 1]
+            pages = Page.objects.filter(in_navigation=True, parent__pk=ancestor.pk).order_by('lft')
+        elif self.level == page.level + 1:
+            pages = Page.objects.filter(in_navigation=True, parent__pk=page.pk).order_by('lft')
+        else:
+            pages = []
+
+        context[self.varname] = pages
+        return ''
+
+@register.tag
+def feincms_get_navi_level(parser, token):
+    try:
+        tag_name, level, switch, obj, dummy, varname = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, "%r tag requires exactly five arguments" % token.contents.split()[0]
+    return NaviLevelNode(level, switch, obj, dummy, varname)
+
diff --git a/feincms/templatetags/utils.py b/feincms/templatetags/utils.py
new file mode 100644
index 0000000..5f3fc9d
--- /dev/null
+++ b/feincms/templatetags/utils.py
@@ -0,0 +1,150 @@
+'''
+I really hate repeating myself. These are helpers that avoid typing the
+whole thing over and over when implementing additional template tags
+
+They help implementing tags of the form
+
+{% tag as var_name %} (SimpleAssignmentNode)
+and
+{% tag of template_var as var_name %} (SimpleAssignmentNodeWithVar)
+'''
+
+from django import template
+
+def _parse_args(argstr):
+    try:
+        args = {}
+        for token in argstr.split(','):
+            k, v = token.split('=')
+            args[k] = v
+
+        return args
+
+    except TypeError:
+        raise template.TemplateSyntaxError('Malformed arguments')
+
+def do_simple_node_with_var_and_args_helper(cls):
+    def _func(parser, token):
+        try:
+            tag_name, of_, in_var_name, args = token.contents.split()
+        except ValueError:
+            raise template.TemplateSyntaxError
+
+        return cls(tag_name, in_var_name, args)
+
+    return _func
+
+class SimpleNodeWithVarAndArgs(template.Node):
+    def __init__(self, tag_name, in_var_name, args):
+        self.tag_name = tag_name
+        self.in_var = template.Variable(in_var_name)
+        self.args = args
+
+    def render(self, context):
+        try:
+            instance = self.in_var.resolve(context)
+        except template.VariableDoesNotExist:
+            return ''
+
+        return self.what(instance, _parse_args(self.args))
+
+def do_simple_node_with_var_helper(cls):
+    def _func(parser, token):
+        try:
+            tag_name, of_, in_var_name = token.contents.split()
+        except ValueError:
+            raise template.TemplateSyntaxError
+
+        return cls(tag_name, in_var_name)
+
+    return _func
+
+class SimpleNodeWithVar(template.Node):
+    def __init__(self, tag_name, in_var_name):
+        self.tag_name = tag_name
+        self.in_var = template.Variable(in_var_name)
+
+    def render(self, context):
+        try:
+            instance = self.in_var.resolve(context)
+        except template.VariableDoesNotExist:
+            return ''
+
+        return self.what(instance)
+
+def do_simple_assignment_node_helper(cls):
+    def _func(parser, token):
+        try:
+            tag_name, as_, var_name = token.contents.split()
+        except ValueError:
+            raise template.TemplateSyntaxError
+
+        return cls(tag_name, var_name)
+
+    return _func
+
+class SimpleAssignmentNode(template.Node):
+    def __init__(self, tag_name, var_name):
+        self.tag_name = tag_name
+        self.var_name = var_name
+
+    def render(self, context):
+        context[self.var_name] = self.what()
+        return ''
+
+def do_simple_assignment_node_with_var_helper(cls):
+    def _func(parser, token):
+        try:
+            tag_name, of_, in_var_name, as_, var_name = token.contents.split()
+        except ValueError:
+            raise template.TemplateSyntaxError
+
+        return cls(tag_name, in_var_name, var_name)
+
+    return _func
+
+class SimpleAssignmentNodeWithVar(template.Node):
+    def __init__(self, tag_name, in_var_name, var_name):
+        self.tag_name = tag_name
+        self.in_var = template.Variable(in_var_name)
+        self.var_name = var_name
+
+    def render(self, context):
+        try:
+            instance = self.in_var.resolve(context)
+        except template.VariableDoesNotExist:
+            context[self.var_name] = []
+            return ''
+
+        context[self.var_name] = self.what(instance)
+        return ''
+
+def do_simple_assignment_node_with_var_and_args_helper(cls):
+    def _func(parser, token):
+        try:
+            tag_name, of_, in_var_name, as_, var_name, args = token.contents.split()
+        except ValueError:
+            raise template.TemplateSyntaxError
+
+        return cls(tag_name, in_var_name, var_name, args)
+
+    return _func
+
+class SimpleAssignmentNodeWithVarAndArgs(template.Node):
+    def __init__(self, tag_name, in_var_name, var_name, args):
+        self.tag_name = tag_name
+        self.in_var = template.Variable(in_var_name)
+        self.var_name = var_name
+        self.args = args
+
+    def render(self, context):
+        try:
+            instance = self.in_var.resolve(context)
+        except template.VariableDoesNotExist:
+            context[self.var_name] = []
+            return ''
+
+        context[self.var_name] = self.what(instance, _parse_args(self.args))
+
+        return ''
+
diff --git a/feincms/views/__init__.py b/feincms/views/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/views/base.py b/feincms/views/base.py
new file mode 100644
index 0000000..47ca887
--- /dev/null
+++ b/feincms/views/base.py
@@ -0,0 +1,23 @@
+from django.http import HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.utils import translation
+
+from feincms.module.page.models import Page
+
+
+def handler(request, path=None):
+    if path is None:
+        path = request.path
+
+    page = Page.objects.page_for_path_or_404(path)
+
+    if page.redirect_to:
+        return HttpResponseRedirect(page.redirect_to)
+
+    page.setup_request(request)
+
+    return render_to_response(page.template.path, {
+        'feincms_page': page,
+        }, context_instance=RequestContext(request))
+
diff --git a/feincms/views/decorators.py b/feincms/views/decorators.py
new file mode 100644
index 0000000..e31bcdd
--- /dev/null
+++ b/feincms/views/decorators.py
@@ -0,0 +1,16 @@
+try:
+    from functools import wraps
+except ImportError:
+    from django.utils.functional import wraps
+
+from feincms.module.page.models import Page
+
+
+def add_page_to_extra_context(view_func):
+    def inner(request, *args, **kwargs):
+        kwargs.setdefault('extra_context', {})
+        kwargs['extra_context']['feincms_page'] = Page.objects.best_match_for_request(request)
+
+        return view_func(request, *args, **kwargs)
+    return wraps(view_func)(inner)
+
diff --git a/feincms/views/generic/__init__.py b/feincms/views/generic/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/feincms/views/generic/create_update.py b/feincms/views/generic/create_update.py
new file mode 100644
index 0000000..8ffe4c7
--- /dev/null
+++ b/feincms/views/generic/create_update.py
@@ -0,0 +1,8 @@
+from django.views.generic import create_update
+from feincms.views.decorators import add_page_to_extra_context
+
+
+create_object = add_page_to_extra_context(create_update.create_object)
+update_object = add_page_to_extra_context(create_update.update_object)
+delete_object = add_page_to_extra_context(create_update.delete_object)
+
diff --git a/feincms/views/generic/date_based.py b/feincms/views/generic/date_based.py
new file mode 100644
index 0000000..1b0474a
--- /dev/null
+++ b/feincms/views/generic/date_based.py
@@ -0,0 +1,12 @@
+from django.views.generic import date_based
+from feincms.views.decorators import add_page_to_extra_context
+
+
+archive_index = add_page_to_extra_context(date_based.archive_index)
+archive_year = add_page_to_extra_context(date_based.archive_year)
+archive_month = add_page_to_extra_context(date_based.archive_month)
+archive_week = add_page_to_extra_context(date_based.archive_week)
+archive_day = add_page_to_extra_context(date_based.archive_day)
+archive_today = add_page_to_extra_context(date_based.archive_today)
+object_detail = add_page_to_extra_context(date_based.object_detail)
+
diff --git a/feincms/views/generic/list_detail.py b/feincms/views/generic/list_detail.py
new file mode 100644
index 0000000..6684e5a
--- /dev/null
+++ b/feincms/views/generic/list_detail.py
@@ -0,0 +1,7 @@
+from django.views.generic import list_detail
+from feincms.views.decorators import add_page_to_extra_context
+
+
+object_list = add_page_to_extra_context(list_detail.object_list)
+object_detail = add_page_to_extra_context(list_detail.object_detail)
+
diff --git a/feincms/views/generic/simple.py b/feincms/views/generic/simple.py
new file mode 100644
index 0000000..22f1b7e
--- /dev/null
+++ b/feincms/views/generic/simple.py
@@ -0,0 +1,6 @@
+from django.views.generic import simple
+from feincms.views.decorators import add_page_to_extra_context
+
+
+direct_to_template = add_page_to_extra_context(simple.direct_to_template)
+
diff --git a/parsers/surveys.py b/parsers/surveys.py
index 290d550..142a2bb 100644
--- a/parsers/surveys.py
+++ b/parsers/surveys.py
@@ -12,6 +12,19 @@ from PIL import Image
 import csv
 import re
 import datetime
+from save_carefully import save_carefully
+
+def get_or_create_placeholder(year):
+    """ All surveys must be related to a logbookentry. We don't have a way to
+        automatically figure out which survey went with which logbookentry,
+        so we create a survey placeholder logbook entry for each year. This
+        function always returns such a placeholder, and creates it if it doesn't
+        exist yet.
+    """
+    lookupAttribs={'date__year':int(year),  'title':"placeholder for surveys",}
+    nonLookupAttribs={'text':"surveys temporarily attached to this should be re-attached to their actual trips", 'date':datetime.date(int(year),1,1)}
+    placeholder_logbook_entry, newly_created = save_carefully(LogbookEntry, lookupAttribs, nonLookupAttribs)
+    return placeholder_logbook_entry
 
 def readSurveysFromCSV(logfile=None):
     try:
@@ -42,13 +55,16 @@ def readSurveysFromCSV(logfile=None):
         logfile.write("Beginning to import surveys from "+str(os.path.join(settings.SURVEYS, "Surveys.csv"))+"\n"+"-"*60+"\n")
     
     for survey in surveyreader:
-        walletNumberLetter = re.match(r'(?P<number>\d*)(?P<letter>[a-zA-Z]*)',survey[header['Survey Number']]) #I hate this, but some surveys have a letter eg 2000#34a. This line deals with that.
+        #I hate this, but some surveys have a letter eg 2000#34a. The next line deals with that.
+        walletNumberLetter = re.match(r'(?P<number>\d*)(?P<letter>[a-zA-Z]*)',survey[header['Survey Number']]) 
     #    print walletNumberLetter.groups()
+        year=survey[header['Year']]
 
+        
         surveyobj = Survey(
-            expedition = Expedition.objects.filter(year=survey[header['Year']])[0],
+            expedition = Expedition.objects.filter(year=year)[0],
             wallet_number = walletNumberLetter.group('number'),
-
+            logbook_entry = get_or_create_placeholder(year),
             comments = survey[header['Comments']],
             location = survey[header['Location']]
             )
@@ -101,7 +117,8 @@ def parseSurveyScans(year, logfile=None):
             if type(surveyNumber)==types.TupleType:
                 surveyNumber=surveyNumber[0]
             try:
-                survey=Survey.objects.get_or_create(wallet_number=surveyNumber, expedition=year)[0]
+                placeholder=get_or_create_placeholder(year=int(year.year))
+                survey=Survey.objects.get_or_create(wallet_number=surveyNumber, expedition=year, defaults={'logbook_entry':placeholder})[0]
             except Survey.MultipleObjectsReturned:
                 survey=Survey.objects.filter(wallet_number=surveyNumber, expedition=year)[0]
             file=os.path.join(year.year, surveyFolder, scan)
diff --git a/settings.py b/settings.py
index a3f5dd4..45d1867 100644
--- a/settings.py
+++ b/settings.py
@@ -86,6 +86,8 @@ INSTALLED_APPS = (
     'troggle.profiles',
     'troggle.expo',
     'troggle.imagekit', 
+    'mptt', #This is django-mptt (modifed preorder tree traversal) which allows the tree structure of subcaves.
+    'feincms' #This is a little content management app that does the javascript admin page for mptt.
 )
 
 from localsettings import * #localsettings needs to take precedence. Call it to override any existing vars.
\ No newline at end of file
diff --git a/templates/survey.html b/templates/survey.html
index 290cd97..4b6ebbc 100644
--- a/templates/survey.html
+++ b/templates/survey.html
@@ -67,7 +67,7 @@
     <div id="progressTable" class="menuBarItem"> {% if current_expedition.survey_set.all %}&#10003;{% endif %}
       survey progress table </div>
   </div>      
-    
+{% if current_survey %}    
    <h3>Choose a wallet number   </h3>
    <center>
      <select id="surveyChooser" class="centre" onChange="redirectSurvey()">
@@ -101,8 +101,10 @@
     <div id="mainSketchIntegration" class="menuBarItem" ">add to main sketch</div>
   </div>
  </div>
+{% endif %}
 {% endblock %}
 
+
 {% block content %}
 <div id="mainContent">