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> › + <a href="../../">{{ opts.app_label|capfirst|escape }}</a> › + <a href="../">{{ opts.verbose_name_plural|capfirst }}</a> › + {{ 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 %}✓{% 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">