Merge branch 'penfield' of git://github.com/kgodey/philo into penfield
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Wed, 16 Jun 2010 15:31:50 +0000 (11:31 -0400)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Wed, 16 Jun 2010 15:31:50 +0000 (11:31 -0400)
* 'penfield' of git://github.com/kgodey/philo:
  The index template gets a queryset of entries now.
  Fixed date ordering.
  Fixed blog entries plural and ordering.

README
__init__.py
admin.py
models.py
templates/admin/philo/edit_inline/tabular_collapse.html [new file with mode: 0644]
templates/admin/philo/page/add_form.html [new file with mode: 0644]
templatetags/containers.py
views.py

diff --git a/README b/README
index 82333d6..4170400 100644 (file)
--- a/README
+++ b/README
@@ -2,7 +2,6 @@ Philo is a foundation for developing web content management systems.
 
 Prerequisites:
        * Python 2.5.4+ <http://www.python.org/>
-       * simplejson <http://code.google.com/p/simplejson/> (Not required with Python 2.6+)
        * Django 1.1.1+ <http://www.djangoproject.com/>
        * (Optional) django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>
 
index 8ad7212..9fc18ae 100644 (file)
@@ -1,4 +1,4 @@
-from models import Template
+from philo.models import Template
 
 
 load_template_source = Template.loader
index fa62647..178a904 100644 (file)
--- a/admin.py
+++ b/admin.py
@@ -7,7 +7,7 @@ from django.utils.translation import ugettext as _
 from django.utils.safestring import mark_safe
 from django.utils.html import escape
 from django.utils.text import truncate_words
-from models import *
+from philo.models import *
 
 
 class AttributeInline(generic.GenericTabularInline):
@@ -15,7 +15,7 @@ class AttributeInline(generic.GenericTabularInline):
        ct_fk_field = 'entity_object_id'
        model = Attribute
        extra = 1
-       classes = ('collapse-closed',)
+       template = 'admin/philo/edit_inline/tabular_collapse.html'
        allow_add = True
 
 
@@ -24,7 +24,7 @@ class RelationshipInline(generic.GenericTabularInline):
        ct_fk_field = 'entity_object_id'
        model = Relationship
        extra = 1
-       classes = ('collapse-closed',)
+       template = 'admin/philo/edit_inline/tabular_collapse.html'
        allow_add = True
 
 
@@ -45,26 +45,8 @@ class CollectionAdmin(admin.ModelAdmin):
        inlines = [CollectionMemberInline]
 
 
-class TemplateAdmin(admin.ModelAdmin):
-       prepopulated_fields = {'slug': ('name',)}
-       fieldsets = (
-               (None, {
-                       'fields': ('parent', 'name', 'slug')
-               }),
-               ('Documentation', {
-                       'classes': ('collapse', 'collapse-closed'),
-                       'fields': ('documentation',)
-               }),
-               (None, {
-                       'fields': ('code',)
-               }),
-               ('Advanced', {
-                       'classes': ('collapse','collapse-closed'),
-                       'fields': ('mimetype',)
-               }),
-       )
-       save_on_top = True
-       save_as = True
+class NodeAdmin(EntityAdmin):
+       pass
 
 
 class ModelLookupWidget(forms.TextInput):
@@ -94,15 +76,41 @@ class ModelLookupWidget(forms.TextInput):
                return mark_safe(output)
 
 
-class PageAdmin(EntityAdmin):
+class RedirectAdmin(NodeAdmin):
+       fieldsets = (
+               (None, {
+                       'fields': ('slug', 'target', 'status_code')
+               }),
+               ('URL/Tree/Hierarchy', {
+                       'classes': ('collapse', 'collapse-closed'),
+                       'fields': ('parent',)
+               }),
+       )
+
+
+class FileAdmin(NodeAdmin):
+       prepopulated_fields = {'slug': ('file',)}
+       fieldsets = (
+               (None, {
+                       'fields': ('file', 'slug', 'mimetype')
+               }),
+               ('URL/Tree/Hierarchy', {
+                       'classes': ('collapse', 'collapse-closed'),
+                       'fields': ('parent',)
+               }),
+       )
+
+
+class PageAdmin(NodeAdmin):
+       add_form_template = 'admin/philo/page/add_form.html'
        prepopulated_fields = {'slug': ('title',)}
        fieldsets = (
                (None, {
-                       'fields': ('title', 'template')
+                       'fields': ('title', 'slug', 'template')
                }),
                ('URL/Tree/Hierarchy', {
                        'classes': ('collapse', 'collapse-closed'),
-                       'fields': ('parent', 'slug')
+                       'fields': ('parent',)
                }),
        )
        list_display = ('title', 'path', 'template')
@@ -112,8 +120,11 @@ class PageAdmin(EntityAdmin):
        def get_fieldsets(self, request, obj=None, **kwargs):
                fieldsets = list(self.fieldsets)
                if obj: # if no obj, creating a new page, thus no template set, thus no containers
-                       page = obj
-                       template = page.template
+                       template = obj.template
+                       if template.documentation:
+                               fieldsets.append(('Template Documentation', {
+                                       'description': template.documentation
+                               }))
                        contentlet_containers, contentreference_containers = template.containers
                        for container_name in contentlet_containers:
                                fieldsets.append((('Container: %s' % container_name), {
@@ -173,8 +184,30 @@ class PageAdmin(EntityAdmin):
                                        contentreference.save()
 
 
+class TemplateAdmin(admin.ModelAdmin):
+       prepopulated_fields = {'slug': ('name',)}
+       fieldsets = (
+               (None, {
+                       'fields': ('parent', 'name', 'slug')
+               }),
+               ('Documentation', {
+                       'classes': ('collapse', 'collapse-closed'),
+                       'fields': ('documentation',)
+               }),
+               (None, {
+                       'fields': ('code',)
+               }),
+               ('Advanced', {
+                       'classes': ('collapse','collapse-closed'),
+                       'fields': ('mimetype',)
+               }),
+       )
+       save_on_top = True
+       save_as = True
+
+
 admin.site.register(Collection, CollectionAdmin)
-admin.site.register(Redirect)
-admin.site.register(File)
+admin.site.register(Redirect, RedirectAdmin)
+admin.site.register(File, FileAdmin)
 admin.site.register(Page, PageAdmin)
 admin.site.register(Template, TemplateAdmin)
index e32265d..ecd1c1f 100644 (file)
--- a/models.py
+++ b/models.py
@@ -5,22 +5,20 @@ from django.contrib.contenttypes import generic
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django.contrib.sites.models import Site
-from utils import fattr
+from philo.utils import fattr
 from django.template import add_to_builtins as register_templatetags
 from django.template import Template as DjangoTemplate
 from django.template import TemplateDoesNotExist
 from django.template import Context, RequestContext
 from django.core.exceptions import ObjectDoesNotExist
-try:
-       import json
-except ImportError:
-       import simplejson as json
+from django.utils import simplejson as json
 from UserDict import DictMixin
-from templatetags.containers import ContainerNode
+from philo.templatetags.containers import ContainerNode
 from django.template.loader_tags import ExtendsNode, ConstantIncludeNode, IncludeNode
 from django.template.loader import get_template
 from django.http import Http404, HttpResponse, HttpResponseServerError, HttpResponseRedirect
 from django.core.servers.basehttp import FileWrapper
+from django.conf import settings
 
 
 def register_value_model(model):
@@ -195,9 +193,18 @@ class TreeEntity(TreeModel, Entity):
                abstract = True
 
 
-class Node(TreeEntity):
+class InheritableTreeEntity(TreeEntity):
        instance_type = models.ForeignKey(ContentType, editable=False)
        
+       def save(self, force_insert=False, force_update=False):
+               if not hasattr(self, 'instance_type_ptr'):
+                       self.instance_type = ContentType.objects.get_for_model(self.__class__)
+               super(InheritableTreeEntity, self).save(force_insert, force_update)
+       
+       @property
+       def instance(self):
+               return self.instance_type.get_object_for_this_type(id=self.id)
+       
        def get_path(self, pathsep='/', field='slug'):
                path = getattr(self.instance, field, '?')
                parent = self.parent
@@ -207,15 +214,23 @@ class Node(TreeEntity):
                return path
        path = property(get_path)
        
-       def save(self, force_insert=False, force_update=False):
-               if not hasattr(self, 'instance_type_ptr'):
-                       self.instance_type = ContentType.objects.get_for_model(self.__class__)
-               super(Node, self).save(force_insert, force_update)
-       
        @property
-       def instance(self):
-               return self.instance_type.get_object_for_this_type(id=self.id)
+       def attributes(self):
+               if self.parent:
+                       return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
+               return QuerySetMapper(self.instance.attribute_set)
+
+       @property
+       def relationships(self):
+               if self.parent:
+                       return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
+               return QuerySetMapper(self.instance.relationship_set)
        
+       class Meta:
+               abstract = True
+
+
+class Node(InheritableTreeEntity):
        accepts_subpath = False
        
        def render_to_response(self, request, path=None, subpath=None):
@@ -244,7 +259,7 @@ class Redirect(Node):
                (302, 'Temporary'),
                (301, 'Permanent'),
        )
-       target = models.URLField()
+       target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
        status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
        
        def render_to_response(self, request, path=None, subpath=None):
@@ -268,8 +283,8 @@ class File(Node):
 class Template(TreeModel):
        name = models.CharField(max_length=255)
        documentation = models.TextField(null=True, blank=True)
-       mimetype = models.CharField(max_length=255, null=True, blank=True)
-       code = models.TextField()
+       mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
+       code = models.TextField(verbose_name='django template code')
        
        @property
        def origin(self):
@@ -292,7 +307,7 @@ class Template(TreeModel):
                                nodes = []
                                for node in nodelist:
                                        try:
-                                               for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false'):
+                                               for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
                                                        if hasattr(node, nodelist_name):
                                                                nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
                                                if isinstance(node, ContainerNode):
@@ -337,6 +352,11 @@ class Template(TreeModel):
 
 
 class Page(Node):
+       """
+       Represents an HTML page. The page will have a number of related Contentlets
+       depending on the template selected - but these will appear only after the
+       page has been saved with that template.
+       """
        template = models.ForeignKey(Template, related_name='pages')
        title = models.CharField(max_length=255)
        
diff --git a/templates/admin/philo/edit_inline/tabular_collapse.html b/templates/admin/philo/edit_inline/tabular_collapse.html
new file mode 100644 (file)
index 0000000..c457297
--- /dev/null
@@ -0,0 +1,125 @@
+{% load i18n adminmedia %}
+<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
+  <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
+{{ inline_admin_formset.formset.management_form }}
+<fieldset class="module collapse collapse-closed">
+   <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
+   {{ inline_admin_formset.formset.non_form_errors }}
+   <table>
+     <thead><tr>
+     {% for field in inline_admin_formset.fields %}
+       {% if not field.widget.is_hidden %}
+         <th{% if forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}</th>
+       {% endif %}
+     {% endfor %}
+     {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
+     </tr></thead>
+
+     <tbody>
+     {% for inline_admin_form in inline_admin_formset %}
+        {% if inline_admin_form.form.non_field_errors %}
+        <tr><td colspan="{{ inline_admin_form.field_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
+        {% endif %}
+        <tr class="{% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %}"
+             id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
+        <td class="original">
+          {% if inline_admin_form.original or inline_admin_form.show_url %}<p>
+          {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
+          {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original_content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
+            </p>{% endif %}
+          {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
+          {{ inline_admin_form.fk_field.field }}
+          {% spaceless %}
+          {% for fieldset in inline_admin_form %}
+            {% for line in fieldset %}
+              {% for field in line %}
+                {% if field.is_hidden %} {{ field.field }} {% endif %}
+              {% endfor %}
+            {% endfor %}
+          {% endfor %}
+          {% endspaceless %}
+        </td>
+        {% for fieldset in inline_admin_form %}
+          {% for line in fieldset %}
+            {% for field in line %}
+              <td class="{{ field.field.name }}">
+              {% if field.is_readonly %}
+                  <p>{{ field.contents }}</p>
+              {% else %}
+                  {{ field.field.errors.as_ul }}
+                  {{ field.field }}
+              {% endif %}
+              </td>
+            {% endfor %}
+          {% endfor %}
+        {% endfor %}
+        {% if inline_admin_formset.formset.can_delete %}
+          <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
+        {% endif %}
+        </tr>
+     {% endfor %}
+     </tbody>
+   </table>
+</fieldset>
+  </div>
+</div>
+
+<script type="text/javascript">
+(function($) {
+    $(document).ready(function($) {
+        var rows = "#{{ inline_admin_formset.formset.prefix }}-group .tabular.inline-related tbody tr";
+        var alternatingRows = function(row) {
+            $(rows).not(".add-row").removeClass("row1 row2")
+                .filter(":even").addClass("row1").end()
+                .filter(rows + ":odd").addClass("row2");
+        }
+        var reinitDateTimeShortCuts = function() {
+            // Reinitialize the calendar and clock widgets by force
+            if (typeof DateTimeShortcuts != "undefined") {
+                $(".datetimeshortcuts").remove();
+                DateTimeShortcuts.init();
+            }
+        }
+        var updateSelectFilter = function() {
+            // If any SelectFilter widgets are a part of the new form,
+            // instantiate a new SelectFilter instance for it.
+            if (typeof SelectFilter != "undefined"){
+                $(".selectfilter").each(function(index, value){
+                  var namearr = value.name.split('-');
+                  SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% admin_media_prefix %}");
+                })
+                $(".selectfilterstacked").each(function(index, value){
+                  var namearr = value.name.split('-');
+                  SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% admin_media_prefix %}");
+                })
+            }
+        }
+        var initPrepopulatedFields = function(row) {
+            row.find('.prepopulated_field').each(function() {
+                var field = $(this);
+                var input = field.find('input, select, textarea');
+                var dependency_list = input.data('dependency_list') || [];
+                var dependencies = row.find(dependency_list.join(',')).find('input, select, textarea');
+                if (dependencies.length) {
+                    input.prepopulate(dependencies, input.attr('maxlength'));
+                }
+            });
+        }
+        $(rows).formset({
+            prefix: "{{ inline_admin_formset.formset.prefix }}",
+            addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",
+            formCssClass: "dynamic-{{ inline_admin_formset.formset.prefix }}",
+            deleteCssClass: "inline-deletelink",
+            deleteText: "{% trans "Remove" %}",
+            emptyCssClass: "empty-form",
+            removed: alternatingRows,
+            added: (function(row) {
+                initPrepopulatedFields(row);
+                reinitDateTimeShortCuts();
+                updateSelectFilter();
+                alternatingRows(row);
+            })
+        });
+    });
+})(django.jQuery);
+</script>
diff --git a/templates/admin/philo/page/add_form.html b/templates/admin/philo/page/add_form.html
new file mode 100644 (file)
index 0000000..8937463
--- /dev/null
@@ -0,0 +1,13 @@
+{% extends "admin/change_form.html" %}
+{% load i18n %}
+
+{% block form_top %}
+       <p>{% trans "First, choose a template. After saving, you'll be able to provide additional content for containers." %}</p>
+       <input type="hidden" name="_continue" value="1" />
+{% endblock %}
+
+{% block content %}
+{% with 0 as save_on_top %}
+{{ block.super }}
+{% endwith %}
+{% endblock %}
\ No newline at end of file
index cc6b313..ca5e1e9 100644 (file)
@@ -9,45 +9,84 @@ register = template.Library()
 
 
 class ContainerNode(template.Node):
-       def __init__(self, name, references=None, as_var=None):
+       child_nodelists = ('nodelist_main', 'nodelist_empty',)
+       
+       def __init__(self, name, references=None, as_var=None, nodelist_main=None, nodelist_empty=None):
                self.name = name
                self.as_var = as_var
                self.references = references
+               
+               if nodelist_main is None:
+                       self.nodelist_main = template.NodeList()
+               else:
+                       self.nodelist_main = nodelist_main
+               
+               if nodelist_empty is None:
+                       self.nodelist_empty = template.NodeList()
+               else:
+                       self.nodelist_empty = nodelist_empty
+       
        def render(self, context):
                content = settings.TEMPLATE_STRING_IF_INVALID
                if 'page' in context:
-                       page = context['page']
-                       if self.references:
-                               try:
-                                       contentreference = page.contentreferences.get(name__exact=self.name, content_type=self.references)
-                                       content = contentreference.content
-                               except ObjectDoesNotExist:
-                                       pass
-                       else:
-                               try:
-                                       contentlet = page.contentlets.get(name__exact=self.name)
-                                       if contentlet.dynamic:
-                                               try:
-                                                       content = mark_safe(template.Template(contentlet.content, name=contentlet.name).render(context))
-                                               except template.TemplateSyntaxError, error:
-                                                       if settings.DEBUG:
-                                                               content = ('[Error parsing contentlet \'%s\': %s]' % self.name, error)
-                                       else:
-                                               content = contentlet.content
-                               except ObjectDoesNotExist:
-                                       pass
-               if content and self.as_var:
-                       context[self.as_var] = content
-                       return ''
+                       container_content = self.get_container_content(context['page'])
+               
+               if self.nodelist_main is None:
+                       self.nodelist_main
+                       if container_content and self.as_var:
+                               context[self.as_var] = container_content
+                               return ''
+                       return container_content
+               
+               if container_content:
+                       if self.as_var is None:
+                               self.as_var = self.name
+                       
+                       #make a new context 
+                       context.push()
+                       context[self.as_var] = container_content
+                       nodelist = template.NodeList()
+                       for node in self.nodelist_main:
+                               nodelist.append(node.render(context))
+                       context.pop()
+                       return nodelist.render(context)
+               
+               if self.nodelist_empty is not None:
+                       return self.nodelist_empty.render(context)
+               
+               return ''
+       
+       def get_container_content(self, page):
+               if self.references:
+                       try:
+                               contentreference = page.contentreferences.get(name__exact=self.name, content_type=self.references)
+                               content = contentreference.content
+                       except ObjectDoesNotExist:
+                               content = ''
+               else:
+                       try:
+                               contentlet = page.contentlets.get(name__exact=self.name)
+                               if contentlet.dynamic:
+                                       try:
+                                               content = mark_safe(template.Template(contentlet.content, name=contentlet.name).render(context))
+                                       except template.TemplateSyntaxError, error:
+                                               if settings.DEBUG:
+                                                       content = ('[Error parsing contentlet \'%s\': %s]' % self.name, error)
+                               else:
+                                       content = contentlet.content
+                       except ObjectDoesNotExist:
+                               content = ''
                return content
 
 
 def do_container(parser, token):
        """
-       {% container <name> [[references <type>] as <variable>] %}
+       {% container <name> [[references <type>] as <variable>] %} 
+       {% blockcontainer <name> [[references <type>] as <variable>] %} [ {% empty %} ] {% endblockcontainer %}
        """
        params = token.split_contents()
        if len(params) >= 2:
+               tag = params[0]
                name = params[1].strip('"')
                references = None
                as_var = None
@@ -60,19 +99,34 @@ def do_container(parser, token):
                                                app_label, model = remaining_tokens.pop(0).strip('"').split('.')
                                                references = ContentType.objects.get(app_label=app_label, model=model)
                                        except IndexError:
-                                               raise template.TemplateSyntaxError('"container" template tag option "references" requires an argument specifying a content type')
+                                               raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument specifying a content type' % tag)
                                        except ValueError:
-                                               raise template.TemplateSyntaxError('"container" template tag option "references" requires an argument of the form app_label.model (see django.contrib.contenttypes)')
+                                               raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument of the form app_label.model (see django.contrib.contenttypes)' % tag)
                                        except ObjectDoesNotExist:
-                                               raise template.TemplateSyntaxError('"container" template tag option "references" requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)')
+                                               raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tag)
                                elif option_token == 'as':
                                        try:
                                                as_var = remaining_tokens.pop(0)
                                        except IndexError:
-                                               raise template.TemplateSyntaxError('"container" template tag option "as" requires an argument specifying a variable name')
+                                               raise template.TemplateSyntaxError('"%s" template tag option "as" requires an argument specifying a variable name' % tag)
                        if references and not as_var:
-                               raise template.TemplateSyntaxError('"container" template tags using "references" option require additional use of the "as" option specifying a variable name')
-               return ContainerNode(name, references, as_var)
+                               raise template.TemplateSyntaxError('"%s" template tags using "references" option require additional use of the "as" option specifying a variable name' % tag)
+               if tag == 'container':
+                       return ContainerNode(name, references, as_var)
+               
+               nodelist_main = parser.parse(('empty','endblockcontainer',))
+               token = parser.next_token()
+               
+               if token.contents == 'empty':
+                       nodelist_empty = parser.parse(('endblockcontainer',))
+                       parser.delete_first_token()
+               else:
+                       nodelist_empty = None
+               return ContainerNode(name, references, as_var, nodelist_main, nodelist_empty)
+               
        else: # error
-               raise template.TemplateSyntaxError('"container" template tag provided without arguments (at least one required)')
-register.tag('container', do_container)
\ No newline at end of file
+               raise template.TemplateSyntaxError('"%s" template tag provided without arguments (at least one required)' % tag)
+
+
+register.tag('container', do_container)
+register.tag('blockcontainer', do_container)
index c03f687..f086bfd 100644 (file)
--- a/views.py
+++ b/views.py
@@ -1,7 +1,7 @@
 from django.http import Http404, HttpResponse
 from django.template import RequestContext
 from django.contrib.sites.models import Site
-from models import Node
+from philo.models import Node
 
 
 def node_view(request, path=None, **kwargs):