Merge branch 'master' into penfield, cleaned up.
authorKriti Godey <kriti.godey@gmail.com>
Fri, 2 Jul 2010 11:51:13 +0000 (17:21 +0530)
committerKriti Godey <kriti.godey@gmail.com>
Fri, 2 Jul 2010 11:51:13 +0000 (17:21 +0530)
22 files changed:
README
__init__.py
admin.py [deleted file]
admin/__init__.py [new file with mode: 0644]
admin/base.py [new file with mode: 0644]
admin/collections.py [new file with mode: 0644]
admin/nodes.py [new file with mode: 0644]
admin/pages.py [new file with mode: 0644]
admin/widgets.py [new file with mode: 0644]
contrib/penfield/admin.py
contrib/penfield/models.py
models.py [deleted file]
models/__init__.py [new file with mode: 0644]
models/base.py [new file with mode: 0644]
models/collections.py [new file with mode: 0644]
models/nodes.py [new file with mode: 0644]
models/pages.py [new file with mode: 0644]
templatetags/collections.py
urls.py
utils.py
validators.py
views.py

diff --git a/README b/README
index 4170400..bf55344 100644 (file)
--- a/README
+++ b/README
@@ -2,7 +2,7 @@ Philo is a foundation for developing web content management systems.
 
 Prerequisites:
        * Python 2.5.4+ <http://www.python.org/>
-       * Django 1.1.1+ <http://www.djangoproject.com/>
+       * Django 1.2+ <http://www.djangoproject.com/>
        * (Optional) django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>
 
 To contribute, please visit the project website <http://philo.ithinksw.org/>.
index 9fc18ae..52956f3 100644 (file)
@@ -1,4 +1,4 @@
-from philo.models import Template
+from philo.models.pages import Template
 
 
 load_template_source = Template.loader
diff --git a/admin.py b/admin.py
deleted file mode 100644 (file)
index 64893d5..0000000
--- a/admin.py
+++ /dev/null
@@ -1,286 +0,0 @@
-from django.contrib import admin
-from django.contrib.contenttypes import generic
-from django.contrib.contenttypes.models import ContentType
-from django import forms
-from django.conf import settings
-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 philo.models import *
-from django.core.exceptions import ValidationError, ObjectDoesNotExist
-from validators import TreeParentValidator, TreePositionValidator
-
-
-COLLAPSE_CLASSES = ('collapse', 'collapse-closed', 'closed',)
-
-
-class AttributeInline(generic.GenericTabularInline):
-       ct_field = 'entity_content_type'
-       ct_fk_field = 'entity_object_id'
-       model = Attribute
-       extra = 1
-       template = 'admin/philo/edit_inline/tabular_collapse.html'
-       allow_add = True
-
-
-class RelationshipInline(generic.GenericTabularInline):
-       ct_field = 'entity_content_type'
-       ct_fk_field = 'entity_object_id'
-       model = Relationship
-       extra = 1
-       template = 'admin/philo/edit_inline/tabular_collapse.html'
-       allow_add = True
-
-
-class EntityAdmin(admin.ModelAdmin):
-       inlines = [AttributeInline, RelationshipInline]
-       save_on_top = True
-
-
-class CollectionMemberInline(admin.TabularInline):
-       fk_name = 'collection'
-       model = CollectionMember
-       extra = 1
-       classes = COLLAPSE_CLASSES
-       allow_add = True
-       fields = ('member_content_type', 'member_object_id', 'index',)
-
-
-class CollectionAdmin(admin.ModelAdmin):
-       inlines = [CollectionMemberInline]
-       list_display = ('name', 'description', 'get_count')
-
-
-class NodeAdmin(EntityAdmin):
-       pass
-
-
-class ModelLookupWidget(forms.TextInput):
-       # is_hidden = False
-       
-       def __init__(self, content_type, attrs=None):
-               self.content_type = content_type
-               super(ModelLookupWidget, self).__init__(attrs)
-       
-       def render(self, name, value, attrs=None):
-               related_url = '../../../%s/%s/' % (self.content_type.app_label, self.content_type.model)
-               if attrs is None:
-                       attrs = {}
-               if not attrs.has_key('class'):
-                       attrs['class'] = 'vForeignKeyRawIdAdminField'
-               output = super(ModelLookupWidget, self).render(name, value, attrs)
-               output += '<a href="%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);">' % (related_url, name)
-               output += '<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup'))
-               output += '</a>'
-               if value:
-                       value_class = self.content_type.model_class()
-                       try:
-                               value_object = value_class.objects.get(pk=value)
-                               output += '&nbsp;<strong>%s</strong>' % escape(truncate_words(value_object, 14))
-                       except value_class.DoesNotExist:
-                               pass
-               return mark_safe(output)
-
-
-class TreeForm(forms.ModelForm):
-       def __init__(self, *args, **kwargs):
-               super(TreeForm, self).__init__(*args, **kwargs)
-               instance = self.instance
-               instance_class = self.get_instance_class()
-               
-               if instance_class is not None:
-                       try:
-                               self.fields['parent'].queryset = instance_class.objects.exclude(id=instance.id)
-                       except ObjectDoesNotExist:
-                               pass
-                       
-               self.fields['parent'].validators = [TreeParentValidator(*self.get_validator_args())]
-       
-       def get_instance_class(self):
-               return self.instance.__class__
-               
-       def get_validator_args(self):
-               return [self.instance]
-       
-       def clean(self):
-               cleaned_data = self.cleaned_data
-               
-               try:
-                       parent = cleaned_data['parent']
-                       slug = cleaned_data['slug']
-                       obj_class = self.get_instance_class()
-                       tpv = TreePositionValidator(parent, slug, obj_class)
-                       tpv(self.instance)
-               except KeyError:
-                       pass
-               
-               return cleaned_data
-
-
-class NodeForm(TreeForm):
-       def get_instance_class(self):
-               return Node
-               
-       def get_validator_args(self):
-               return [self.instance, 'instance']
-
-
-class PageAdminForm(NodeForm):
-       class Meta:
-               model = Page
-
-
-class RedirectAdminForm(NodeForm):
-       class Meta:
-               model = Redirect
-
-
-class FileAdminForm(NodeForm):
-       class Meta:
-               model = File
-
-
-class RedirectAdmin(NodeAdmin):
-       fieldsets = (
-               (None, {
-                       'fields': ('slug', 'target', 'status_code')
-               }),
-               ('URL/Tree/Hierarchy', {
-                       'classes': COLLAPSE_CLASSES,
-                       'fields': ('parent',)
-               }),
-       )
-       list_display=('slug', 'target', 'path', 'status_code',)
-       list_filter=('status_code',)
-       form = RedirectAdminForm
-
-
-class FileAdmin(NodeAdmin):
-       prepopulated_fields = {'slug': ('file',)}
-       fieldsets = (
-               (None, {
-                       'fields': ('file', 'slug', 'mimetype')
-               }),
-               ('URL/Tree/Hierarchy', {
-                       'classes': COLLAPSE_CLASSES,
-                       'fields': ('parent',)
-               }),
-       )
-       form=FileAdminForm
-       list_display=('slug', 'mimetype', 'path', 'file',)
-
-
-class PageAdmin(NodeAdmin):
-       add_form_template = 'admin/philo/page/add_form.html'
-       prepopulated_fields = {'slug': ('title',)}
-       fieldsets = (
-               (None, {
-                       'fields': ('title', 'slug', 'template')
-               }),
-               ('URL/Tree/Hierarchy', {
-                       'classes': COLLAPSE_CLASSES,
-                       'fields': ('parent',)
-               }),
-       )
-       list_display = ('title', 'path', 'template')
-       list_filter = ('template',)
-       search_fields = ['title', 'slug', 'contentlets__content']
-       form = PageAdminForm
-       
-       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
-                       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), {
-                                       'fields': (('contentlet_container_content_%s' % container_name), ('contentlet_container_dynamic_%s' % container_name))
-                               }))
-                       for container_name, container_content_type in contentreference_containers:
-                               fieldsets.append((('Container: %s' % container_name), {
-                                       'fields': (('contentreference_container_%s' % container_name),)
-                               }))
-               return fieldsets
-       
-       def get_form(self, request, obj=None, **kwargs):
-               form = super(PageAdmin, self).get_form(request, obj, **kwargs)
-               if obj: # if no obj, creating a new page, thus no template set, thus no containers
-                       page = obj
-                       template = page.template
-                       contentlet_containers, contentreference_containers = template.containers
-                       for container_name in contentlet_containers:
-                               initial_content = None
-                               initial_dynamic = False
-                               try:
-                                       contentlet = page.contentlets.get(name__exact=container_name)
-                                       initial_content = contentlet.content
-                                       initial_dynamic = contentlet.dynamic
-                               except Contentlet.DoesNotExist:
-                                       pass
-                               form.base_fields[('contentlet_container_content_%s' % container_name)] = forms.CharField(label='Content', widget=forms.Textarea(), initial=initial_content, required=False)
-                               form.base_fields[('contentlet_container_dynamic_%s' % container_name)] = forms.BooleanField(label='Dynamic', help_text='Specify whether this content contains dynamic template code', initial=initial_dynamic, required=False)
-                       for container_name, container_content_type in contentreference_containers:
-                               initial_content = None
-                               try:
-                                       initial_content = page.contentreferences.get(name__exact=container_name, content_type=container_content_type).content.pk
-                               except ContentReference.DoesNotExist:
-                                       pass
-                               form.base_fields[('contentreference_container_%s' % container_name)] = forms.ModelChoiceField(label='References', widget=ModelLookupWidget(container_content_type), initial=initial_content, required=False, queryset=container_content_type.model_class().objects.all())
-               return form
-       
-       def save_model(self, request, page, form, change):
-               page.save()
-               template = page.template
-               contentlet_containers, contentreference_containers = template.containers
-               for container_name in contentlet_containers:
-                       if (('contentlet_container_content_%s' % container_name) in form.cleaned_data) and (('contentlet_container_dynamic_%s' % container_name) in form.cleaned_data):
-                               content = form.cleaned_data[('contentlet_container_content_%s' % container_name)]
-                               dynamic = form.cleaned_data[('contentlet_container_dynamic_%s' % container_name)]
-                               contentlet, created = page.contentlets.get_or_create(name=container_name, defaults={'content': content, 'dynamic': dynamic})
-                               if not created:
-                                       contentlet.content = content
-                                       contentlet.dynamic = dynamic
-                                       contentlet.save()
-               for container_name, container_content_type in contentreference_containers:
-                       if ('contentreference_container_%s' % container_name) in form.cleaned_data:
-                               content = form.cleaned_data[('contentreference_container_%s' % container_name)]
-                               contentreference, created = page.contentreferences.get_or_create(name=container_name, defaults={'content': content})
-                               if not created:
-                                       contentreference.content = content
-                                       contentreference.save()
-
-
-class TemplateAdmin(admin.ModelAdmin):
-       prepopulated_fields = {'slug': ('name',)}
-       fieldsets = (
-               (None, {
-                       'fields': ('parent', 'name', 'slug')
-               }),
-               ('Documentation', {
-                       'classes': COLLAPSE_CLASSES,
-                       'fields': ('documentation',)
-               }),
-               (None, {
-                       'fields': ('code',)
-               }),
-               ('Advanced', {
-                       'classes': COLLAPSE_CLASSES,
-                       'fields': ('mimetype',)
-               }),
-       )
-       save_on_top = True
-       save_as = True
-       list_display = ('__unicode__', 'slug', 'get_path',)
-       form = TreeForm
-
-
-admin.site.register(Collection, CollectionAdmin)
-admin.site.register(Redirect, RedirectAdmin)
-admin.site.register(File, FileAdmin)
-admin.site.register(Page, PageAdmin)
-admin.site.register(Template, TemplateAdmin)
diff --git a/admin/__init__.py b/admin/__init__.py
new file mode 100644 (file)
index 0000000..9039448
--- /dev/null
@@ -0,0 +1,4 @@
+from philo.admin.base import *
+from philo.admin.collections import *
+from philo.admin.nodes import *
+from philo.admin.pages import *
\ No newline at end of file
diff --git a/admin/base.py b/admin/base.py
new file mode 100644 (file)
index 0000000..bdf9f38
--- /dev/null
@@ -0,0 +1,32 @@
+from django.contrib import admin
+from django.contrib.contenttypes import generic
+from philo.models import Tag, Attribute, Relationship
+
+
+COLLAPSE_CLASSES = ('collapse', 'collapse-closed', 'closed',)
+
+
+class AttributeInline(generic.GenericTabularInline):
+       ct_field = 'entity_content_type'
+       ct_fk_field = 'entity_object_id'
+       model = Attribute
+       extra = 1
+       template = 'admin/philo/edit_inline/tabular_collapse.html'
+       allow_add = True
+
+
+class RelationshipInline(generic.GenericTabularInline):
+       ct_field = 'entity_content_type'
+       ct_fk_field = 'entity_object_id'
+       model = Relationship
+       extra = 1
+       template = 'admin/philo/edit_inline/tabular_collapse.html'
+       allow_add = True
+
+
+class EntityAdmin(admin.ModelAdmin):
+       inlines = [AttributeInline, RelationshipInline]
+       save_on_top = True
+
+
+admin.site.register(Tag)
\ No newline at end of file
diff --git a/admin/collections.py b/admin/collections.py
new file mode 100644 (file)
index 0000000..dfc4826
--- /dev/null
@@ -0,0 +1,20 @@
+from django.contrib import admin
+from philo.admin.base import COLLAPSE_CLASSES
+from philo.models import CollectionMember, Collection
+
+
+class CollectionMemberInline(admin.TabularInline):
+       fk_name = 'collection'
+       model = CollectionMember
+       extra = 1
+       classes = COLLAPSE_CLASSES
+       allow_add = True
+       fields = ('member_content_type', 'member_object_id', 'index')
+
+
+class CollectionAdmin(admin.ModelAdmin):
+       inlines = [CollectionMemberInline]
+       list_display = ('name', 'description', 'get_count')
+
+
+admin.site.register(Collection, CollectionAdmin)
\ No newline at end of file
diff --git a/admin/nodes.py b/admin/nodes.py
new file mode 100644 (file)
index 0000000..093537e
--- /dev/null
@@ -0,0 +1,35 @@
+from django.contrib import admin
+from philo.admin.base import EntityAdmin
+from philo.models import Node, Redirect, File
+
+
+class NodeAdmin(EntityAdmin):
+       pass
+
+
+class ViewAdmin(EntityAdmin):
+       pass
+
+
+class RedirectAdmin(ViewAdmin):
+       fieldsets = (
+               (None, {
+                       'fields': ('target', 'status_code')
+               }),
+       )
+       list_display = ('target', 'status_code')
+       list_filter = ('status_code',)
+
+
+class FileAdmin(ViewAdmin):
+       fieldsets = (
+               (None, {
+                       'fields': ('file', 'mimetype')
+               }),
+       )
+       list_display = ('mimetype', 'file')
+
+
+admin.site.register(Node, NodeAdmin)
+admin.site.register(Redirect, RedirectAdmin)
+admin.site.register(File, FileAdmin)
\ No newline at end of file
diff --git a/admin/pages.py b/admin/pages.py
new file mode 100644 (file)
index 0000000..fc60ad1
--- /dev/null
@@ -0,0 +1,118 @@
+from django.contrib import admin
+from django import forms
+from philo.admin import widgets
+from philo.admin.base import COLLAPSE_CLASSES
+from philo.admin.nodes import ViewAdmin
+from philo.models.pages import Page, Template, Contentlet, ContentReference
+
+
+class PageAdmin(ViewAdmin):
+       add_form_template = 'admin/philo/page/add_form.html'
+       fieldsets = (
+               (None, {
+                       'fields': ('title', 'template')
+               }),
+       )
+       list_display = ('title', 'template')
+       list_filter = ('template',)
+       search_fields = ['title', 'contentlets__content']
+       
+       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
+                       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), {
+                                       'fields': (('contentlet_container_content_%s' % container_name), ('contentlet_container_dynamic_%s' % container_name))
+                               }))
+                       for container_name, container_content_type in contentreference_containers:
+                               fieldsets.append((('Container: %s' % container_name), {
+                                       'fields': (('contentreference_container_%s' % container_name),)
+                               }))
+               return fieldsets
+       
+       def get_form(self, request, obj=None, **kwargs):
+               form = super(PageAdmin, self).get_form(request, obj, **kwargs)
+               if obj: # if no obj, creating a new page, thus no template set, thus no containers
+                       page = obj
+                       template = page.template
+                       contentlet_containers, contentreference_containers = template.containers
+                       for container_name in contentlet_containers:
+                               initial_content = None
+                               initial_dynamic = False
+                               try:
+                                       contentlet = page.contentlets.get(name__exact=container_name)
+                                       initial_content = contentlet.content
+                                       initial_dynamic = contentlet.dynamic
+                               except Contentlet.DoesNotExist:
+                                       pass
+                               form.base_fields[('contentlet_container_content_%s' % container_name)] = forms.CharField(label='Content', widget=forms.Textarea(), initial=initial_content, required=False)
+                               form.base_fields[('contentlet_container_dynamic_%s' % container_name)] = forms.BooleanField(label='Dynamic', help_text='Specify whether this content contains dynamic template code', initial=initial_dynamic, required=False)
+                       for container_name, container_content_type in contentreference_containers:
+                               initial_content = None
+                               try:
+                                       initial_content = page.contentreferences.get(name__exact=container_name, content_type=container_content_type).content.pk
+                               except (ContentReference.DoesNotExist, AttributeError):
+                                       pass
+                               form.base_fields[('contentreference_container_%s' % container_name)] = forms.ModelChoiceField(label='References', widget=widgets.ModelLookupWidget(container_content_type), initial=initial_content, required=False, queryset=container_content_type.model_class().objects.all())
+               return form
+       
+       def save_model(self, request, page, form, change):
+               page.save()
+               template = page.template
+               contentlet_containers, contentreference_containers = template.containers
+               for container_name in contentlet_containers:
+                       if (('contentlet_container_content_%s' % container_name) in form.cleaned_data) and (('contentlet_container_dynamic_%s' % container_name) in form.cleaned_data):
+                               content = form.cleaned_data[('contentlet_container_content_%s' % container_name)]
+                               dynamic = form.cleaned_data[('contentlet_container_dynamic_%s' % container_name)]
+                               contentlet, created = page.contentlets.get_or_create(name=container_name, defaults={'content': content, 'dynamic': dynamic})
+                               if not created:
+                                       contentlet.content = content
+                                       contentlet.dynamic = dynamic
+                                       contentlet.save()
+               for container_name, container_content_type in contentreference_containers:
+                       if ('contentreference_container_%s' % container_name) in form.cleaned_data:
+                               content = form.cleaned_data[('contentreference_container_%s' % container_name)]
+                               try:
+                                       contentreference = page.contentreferences.get(name=container_name)
+                               except ContentReference.DoesNotExist:
+                                       contentreference = ContentReference(name=container_name, page=page, content_type=container_content_type)
+                               
+                               if content == None:
+                                       contentreference.content_id = None
+                               else:
+                                       contentreference.content_id = content.id
+                               
+                               contentreference.save()
+
+
+class TemplateAdmin(admin.ModelAdmin):
+       prepopulated_fields = {'slug': ('name',)}
+       fieldsets = (
+               (None, {
+                       'fields': ('parent', 'name', 'slug')
+               }),
+               ('Documentation', {
+                       'classes': COLLAPSE_CLASSES,
+                       'fields': ('documentation',)
+               }),
+               (None, {
+                       'fields': ('code',)
+               }),
+               ('Advanced', {
+                       'classes': COLLAPSE_CLASSES,
+                       'fields': ('mimetype',)
+               }),
+       )
+       save_on_top = True
+       save_as = True
+       list_display = ('__unicode__', 'slug', 'get_path',)
+
+
+admin.site.register(Page, PageAdmin)
+admin.site.register(Template, TemplateAdmin)
\ No newline at end of file
diff --git a/admin/widgets.py b/admin/widgets.py
new file mode 100644 (file)
index 0000000..f8799fe
--- /dev/null
@@ -0,0 +1,33 @@
+from django import forms
+from django.conf import settings
+from django.utils.translation import ugettext as _
+from django.utils.safestring import mark_safe
+from django.utils.text import truncate_words
+from django.utils.html import escape
+
+
+class ModelLookupWidget(forms.TextInput):
+       # is_hidden = False
+       
+       def __init__(self, content_type, attrs=None):
+               self.content_type = content_type
+               super(ModelLookupWidget, self).__init__(attrs)
+       
+       def render(self, name, value, attrs=None):
+               related_url = '../../../%s/%s/' % (self.content_type.app_label, self.content_type.model)
+               if attrs is None:
+                       attrs = {}
+               if not attrs.has_key('class'):
+                       attrs['class'] = 'vForeignKeyRawIdAdminField'
+               output = super(ModelLookupWidget, self).render(name, value, attrs)
+               output += '<a href="%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);">' % (related_url, name)
+               output += '<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup'))
+               output += '</a>'
+               if value:
+                       value_class = self.content_type.model_class()
+                       try:
+                               value_object = value_class.objects.get(pk=value)
+                               output += '&nbsp;<strong>%s</strong>' % escape(truncate_words(value_object, 14))
+                       except value_class.DoesNotExist:
+                               pass
+               return mark_safe(output)
\ No newline at end of file
index 0e15287..ba885fd 100644 (file)
@@ -1,6 +1,6 @@
-from models import BlogEntry, Blog, BlogNode
 from django.contrib import admin
 from philo.admin import EntityAdmin
+from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView
 
 
 class TitledAdmin(EntityAdmin):
@@ -16,10 +16,30 @@ class BlogEntryAdmin(TitledAdmin):
        pass
 
 
-class BlogNodeAdmin(EntityAdmin):
+class BlogViewAdmin(EntityAdmin):
+       pass
+
+
+class NewsletterAdmin(TitledAdmin):
+       pass
+
+
+class NewsletterArticleAdmin(TitledAdmin):
+       pass
+
+
+class NewsletterIssueAdmin(TitledAdmin):
+       pass
+
+
+class NewsletterViewAdmin(EntityAdmin):
        pass
 
 
 admin.site.register(Blog, BlogAdmin)
 admin.site.register(BlogEntry, BlogEntryAdmin)
-admin.site.register(BlogNode, BlogNodeAdmin)
\ No newline at end of file
+admin.site.register(BlogView, BlogViewAdmin)
+admin.site.register(Newsletter, NewsletterAdmin)
+admin.site.register(NewsletterArticle, NewsletterArticleAdmin)
+admin.site.register(NewsletterIssue, NewsletterIssueAdmin)
+admin.site.register(NewsletterView, NewsletterViewAdmin)
\ No newline at end of file
index a5ba85a..8d7e3ff 100644 (file)
@@ -1,55 +1,38 @@
 from django.db import models
-from philo.models import Entity, MultiNode, Template, register_value_model
-from django.contrib.auth.models import User
+from django.conf import settings
+from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model
 from django.conf.urls.defaults import url, patterns
 from django.http import Http404, HttpResponse
-from django.template import RequestContext
 from datetime import datetime
-from utils import paginate
-from validators import validate_pagination_count
-
-
-class Tag(models.Model):
-       name = models.CharField(max_length=250)
-       slug = models.SlugField()
-       
-       def __unicode__(self):
-               return self.name
-
-
-class Titled(models.Model):
-       title = models.CharField(max_length=255)
-       slug = models.SlugField()
-       
-       def __unicode__(self):
-               return self.title
-       
-       class Meta:
-               abstract = True
+from philo.contrib.penfield.utils import paginate
+from philo.contrib.penfield.validators import validate_pagination_count
 
 
 class Blog(Entity, Titled):
-       pass
+       @property
+       def entry_tags(self):
+               """ Returns a QuerySet of Tags that are used on any entries in this blog. """
+               return Tag.objects.filter(blogentries__blog=self)
 
 
 class BlogEntry(Entity, Titled):
        blog = models.ForeignKey(Blog, related_name='entries')
-       author = models.ForeignKey(User, related_name='blogentries')
+       author = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='blogentries')
        date = models.DateTimeField(default=datetime.now)
        content = models.TextField()
        excerpt = models.TextField()
-       tags = models.ManyToManyField(Tag)
+       tags = models.ManyToManyField(Tag, related_name='blogentries')
        
        class Meta:
                ordering = ['-date']
-               verbose_name_plural = "Blog Entries"
+               verbose_name_plural = "blog entries"
 
 
 register_value_model(BlogEntry)
 
 
-class BlogNode(MultiNode):
-       PERMALINK_STYLE_CHOICES = (
+class BlogView(MultiView):
+       ENTRY_PERMALINK_STYLE_CHOICES = (
                ('D', 'Year, month, and day'),
                ('M', 'Year and month'),
                ('Y', 'Year'),
@@ -57,15 +40,16 @@ class BlogNode(MultiNode):
                ('N', 'No base')
        )
        
-       blog = models.ForeignKey(Blog, related_name='nodes')
+       blog = models.ForeignKey(Blog, related_name='blogviews')
        
-       index_template = models.ForeignKey(Template, related_name='blog_index_related')
-       archive_template = models.ForeignKey(Template, related_name='blog_archive_related')
-       tag_template = models.ForeignKey(Template, related_name='blog_tag_related')
+       index_page = models.ForeignKey(Page, related_name='blog_index_related')
+       entry_page = models.ForeignKey(Page, related_name='blog_entry_related')
+       entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True)
+       tag_page = models.ForeignKey(Page, related_name='blog_tag_related')
+       tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True)
        entries_per_page = models.IntegerField(blank=True, validators=[validate_pagination_count])
-       entry_template = models.ForeignKey(Template, related_name='blog_entry_related')
        
-       entry_permalink_style = models.CharField(max_length=1, choices=PERMALINK_STYLE_CHOICES)
+       entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
        entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
        tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
        
@@ -73,48 +57,51 @@ class BlogNode(MultiNode):
        def urlpatterns(self):
                base_patterns = patterns('',
                        url(r'^$', self.index_view),
-                       url((r'^(?:%s)/?' % self.tag_permalink_base), self.tag_view),
-                       url((r'^(?:%s)/(?P<tag>>[-\w]+)/?' % self.tag_permalink_base), self.tag_view)
+                       url((r'^(?:%s)/?$' % self.tag_permalink_base), self.tag_archive_view),
+                       url((r'^(?:%s)/(?P<tag>[-\w]+)/?$' % self.tag_permalink_base), self.tag_view)
                )
                if self.entry_permalink_style == 'D':
                        entry_patterns = patterns('',
-                               url(r'^(?P<year>\d{4})/?$', self.archive_view),
-                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/?$', self.archive_view),
-                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d+)/?$', self.archive_view),
-                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d+)/(?P<slug>[-\w]+)/?', self.entry_view)
+                               url(r'^(?P<year>\d{4})/?$', self.entry_archive_view),
+                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/?$', self.entry_archive_view),
+                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/?$', self.entry_archive_view),
+                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)/?$', self.entry_view)
                        )
                elif self.entry_permalink_style == 'M':
                        entry_patterns = patterns('',
-                               url(r'^(?P<year>\d{4})/?$', self.archive_view),
-                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/?$', self.archive_view),
-                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>>[-\w]+)/?', self.entry_view)
+                               url(r'^(?P<year>\d{4})/?$', self.entry_archive_view),
+                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/?$', self.entry_archive_view),
+                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)/?$', self.entry_view)
                        )
                elif self.entry_permalink_style == 'Y':
                        entry_patterns = patterns('',
-                               url(r'^(?P<year>\d{4})/?$', self.archive_view),
-                               url(r'^(?P<year>\d{4})/(?P<slug>>[-\w]+)/?', self.entry_view)
+                               url(r'^(?P<year>\d{4})/?$', self.entry_archive_view),
+                               url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)/?$', self.entry_view)
                        )
                elif self.entry_permalink_style == 'B':
                        entry_patterns = patterns('',
-                               url((r'^(?:%s)/?' % self.entry_permalink_base), self.archive_view),
-                               url((r'^(?:%s)/(?P<slug>>[-\w]+)/?' % self.entry_permalink_base), self.entry_view)
+                               url((r'^(?:%s)/?$' % self.entry_permalink_base), self.entry_archive_view),
+                               url((r'^(?:%s)/(?P<slug>[-\w]+)/?$' % self.entry_permalink_base), self.entry_view)
                        )
                else:
                        entry_patterns = patterns('',
-                               url(r'^(?P<slug>>[-\w]+)/?', self.entry_view)
+                               url(r'^(?P<slug>[-\w]+)/?$', self.entry_view)
                        )
                return base_patterns + entry_patterns
        
-       def index_view(self, request):
-               entries = self.blog.entries.order_by('-date')
+       def index_view(self, request, node=None, extra_context=None):
+               entries = self.blog.entries.all()
                if self.entries_per_page:
-                       page = paginate(request, entries, self.entries_per_page)
-                       entries = page.object_list
+                       paginated_page = paginate(request, entries, self.entries_per_page)
+                       entries = paginated_page.object_list
                else:
-                       page = None
-               return HttpResponse(self.index_template.django_template.render(RequestContext(request, {'blog': self.blog, 'entries': entries, 'page': page})), mimetype=self.index_template.mimetype)
+                       paginated_page = None
+               context = {}
+               context.update(extra_context or {})
+               context.update({'blog': self.blog, 'entries': entries, 'paginated_page': paginated_page})
+               return self.index_page.render_to_response(node, request, extra_context=context)
        
-       def archive_view(self, request, year=None, month=None, day=None):
+       def entry_view(self, request, slug, year=None, month=None, day=None, node=None, extra_context=None):
                entries = self.blog.entries.all()
                if year:
                        entries = entries.filter(date__year=year)
@@ -122,24 +109,18 @@ class BlogNode(MultiNode):
                        entries = entries.filter(date__month=month)
                if day:
                        entries = entries.filter(date__day=day)
-               if self.entries_per_page:
-                       page = paginate(request, entries, self.entries_per_page)
-                       entries = page.object_list
-               else:
-                       page = None
-               return HttpResponse(self.archive_template.django_template.render(RequestContext(request, {'blog': self.blog, 'year': year, 'month': month, 'day': day, 'entries': entries, 'page': page})), mimetype=self.archive_template.mimetype)
-       
-       def tag_view(self, request, tag=None):
-               entries = self.blog.entries.filter(tags__slug = tag)
-               if self.entries_per_page:
-                       page = paginate(request, entries, self.entries_per_page)
-                       entries = page.object_list
-               else:
-                       page = None
-               return HttpResponse(self.tag_template.django_template.render(RequestContext(request, {'blog': self.blog, 'tag': tag, 'entries': entries, 'page': page})), mimetype=self.tag_template.mimetype)
-               raise Http404
+               try:
+                       entry = entries.get(slug=slug)
+               except:
+                       raise Http404
+               context = {}
+               context.update(extra_context or {})
+               context.update({'blog': self.blog, 'entry': entry})
+               return self.entry_page.render_to_response(node, request, extra_context=context)
        
-       def entry_view(self, request, slug, year=None, month=None, day=None):
+       def entry_archive_view(self, request, year=None, month=None, day=None, node=None, extra_context=None):
+               if not self.entry_archive_page:
+                       raise Http404
                entries = self.blog.entries.all()
                if year:
                        entries = entries.filter(date__year=year)
@@ -147,23 +128,179 @@ class BlogNode(MultiNode):
                        entries = entries.filter(date__month=month)
                if day:
                        entries = entries.filter(date__day=day)
+               if self.entries_per_page:
+                       paginated_page = paginate(request, entries, self.entries_per_page)
+                       entries = paginated_page.object_list
+               else:
+                       paginated_page = None
+               context = {}
+               context.update(extra_context or {})
+               context.update({'blog': self.blog, 'year': year, 'month': month, 'day': day, 'entries': entries, 'paginated_page': paginated_page})
+               return self.entry_archive_page.render_to_response(node, request, extra_context=context)
+       
+       def tag_view(self, request, tag, node=None, extra_context=None):
                try:
-                       entry = entries.get(slug=slug)
+                       tag = self.blog.entry_tags.filter(slug=tag)
                except:
                        raise Http404
-               return HttpResponse(self.entry_template.django_template.render(RequestContext(request, {'blog': self.blog, 'entry': entry})), mimetype=self.entry_template.mimetype)
+               entries = self.blog.entries.filter(tags=tag)
+               if self.entries_per_page:
+                       paginated_page = paginate(request, entries, self.entries_per_page)
+                       entries = paginated_page.object_list
+               else:
+                       paginated_page = None
+               context = {}
+               context.update(extra_context or {})
+               context.update({'blog': self.blog, 'tag': tag, 'entries': entries, 'paginated_page': paginated_page})
+               return self.tag_page.render_to_response(node, request, extra_context=context)
+       
+       def tag_archive_view(self, request, node=None, extra_context=None):
+               if not self.tag_archive_page:
+                       raise Http404
+               context = {}
+               context.update(extra_context or {})
+               context.update({'blog': self.blog})
+               return self.tag_archive_page.render_to_response(node, request, extra_context=context)
 
 
 class Newsletter(Entity, Titled):
        pass
 
 
-class NewsStory(Entity, Titled):
-       newsletter = models.ForeignKey(Newsletter, related_name='stories')
-       authors = models.ManyToManyField(User, related_name='newsstories')
+class NewsletterArticle(Entity, Titled):
+       newsletter = models.ForeignKey(Newsletter, related_name='articles')
+       authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
        date = models.DateTimeField(default=datetime.now)
        lede = models.TextField(null=True, blank=True)
        full_text = models.TextField()
+       
+       class Meta:
+               ordering = ['-date']
+               unique_together = (('newsletter', 'slug'),)
+
+
+register_value_model(NewsletterArticle)
+
+
+class NewsletterIssue(Entity, Titled):
+       newsletter = models.ForeignKey(Newsletter, related_name='issues')
+       number = models.PositiveIntegerField()
+       articles = models.ManyToManyField(NewsletterArticle)
+       
+       class Meta:
+               ordering = ['-number']
+               unique_together = (('newsletter', 'number'),)
 
 
-register_value_model(NewsStory)
\ No newline at end of file
+class NewsletterView(MultiView):
+       ARTICLE_PERMALINK_STYLE_CHOICES = (
+               ('D', 'Year, month, and day'),
+               ('M', 'Year and month'),
+               ('Y', 'Year'),
+               ('S', 'Slug only')
+       )
+       
+       newsletter = models.ForeignKey(Newsletter, related_name='newsletterviews')
+       
+       index_page = models.ForeignKey(Page, related_name='newsletter_index_related')
+       article_page = models.ForeignKey(Page, related_name='newsletter_article_related')
+       article_archive_page = models.ForeignKey(Page, related_name='newsletter_article_archive_related', null=True, blank=True)
+       issue_page = models.ForeignKey(Page, related_name='newsletter_issue_related')
+       issue_archive_page = models.ForeignKey(Page, related_name='newsletter_issue_archive_related', null=True, blank=True)
+       
+       article_permalink_style = models.CharField(max_length=1, choices=ARTICLE_PERMALINK_STYLE_CHOICES)
+       article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
+       issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
+       
+       @property
+       def urlpatterns(self):
+               base_patterns = patterns('',
+                       url(r'^$', self.index_view),
+                       url((r'^(?:%s)/?$' % self.issue_permalink_base), self.issue_archive_view),
+                       url((r'^(?:%s)/(?P<number>\d+)/?$' % self.issue_permalink_base), self.issue_view)
+               )
+               article_patterns = patterns('',
+                       url((r'^(?:%s)/?$' % self.article_permalink_base), self.article_archive_view)
+               )
+               if self.article_permalink_style in 'DMY':
+                       article_patterns += patterns('',
+                               url((r'^(?:%s)/(?P<year>\d{4})/?$' % self.article_permalink_base), self.article_archive_view)
+                       )
+                       if self.article_permalink_style in 'DM':
+                               article_patterns += patterns('',
+                                       url((r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/?$' % self.article_permalink_base), self.article_archive_view)
+                               )
+                               if self.article_permalink_style == 'D':
+                                       article_patterns += patterns('',
+                                               url((r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/?$' % self.article_permalink_base), self.article_archive_view),
+                                               url((r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)/?$' % self.article_permalink_base), self.article_view)
+                                       )
+                               else:
+                                       article_patterns += patterns('',
+                                               url((r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)/?$' % self.article_permalink_base), self.article_view)
+                                       )
+                       else:
+                               article_patterns += patterns('',
+                                       url((r'^(?:%s)/(?P<year>\d{4})/(?P<slug>[-\w]+)/?$' % self.article_permalink_base), self.article_view)
+                               )
+               else:
+                       article_patterns += patterns('',
+                               url((r'^(?:%s)/(?P<slug>[-\w]+)/?$' % self.article_permalink_base), self.article_view)
+                       )
+               return base_patterns + article_patterns
+       
+       def index_view(self, request, node=None, extra_context=None):
+               context = {}
+               context.update(extra_context or {})
+               context.update({'newsletter': self.newsletter})
+               return self.index_page.render_to_response(node, request, extra_context=context)
+       
+       def article_view(self, request, slug, year=None, month=None, day=None, node=None, extra_context=None):
+               articles = self.newsletter.articles.all()
+               if year:
+                       articles = articles.filter(date__year=year)
+               if month:
+                       articles = articles.filter(date__month=month)
+               if day:
+                       articles = articles.filter(date__day=day)
+               try:
+                       article = articles.get(slug=slug)
+               except:
+                       raise Http404
+               context = {}
+               context.update(extra_context or {})
+               context.update({'newsletter': self.newsletter, 'article': article})
+               return self.article_page.render_to_response(node, request, extra_context=context)
+       
+       def article_archive_view(self, request, year=None, month=None, day=None, node=None, extra_context=None):
+               if not self.article_archive_page:
+                       raise Http404
+               articles = self.newsletter.articles.all()
+               if year:
+                       articles = articles.filter(date__year=year)
+               if month:
+                       articles = articles.filter(date__month=month)
+               if day:
+                       articles = articles.filter(date__day=day)
+               context = {}
+               context.update(extra_context or {})
+               context.update({'newsletter': self.newsletter, 'year': year, 'month': month, 'day': day, 'articles': articles})
+               return self.article_archive_page.render_to_response(node, request, extra_context=context)
+       
+       def issue_view(self, request, number, node=None, extra_context=None):
+               try:
+                       issue = self.newsletter.issues.get(number=number)
+               except:
+                       raise Http404
+               context = {}
+               context.update(extra_context or {})
+               context.update({'newsletter': self.newsletter, 'issue': issue})
+               return self.issue_page.render_to_response(node, request, extra_context=context)
+       
+       def issue_archive_view(self, request, node=None, extra_context=None):
+               if not self.issue_archive_page:
+                       raise Http404
+               context = {}
+               context.update(extra_context or {})
+               context.update({'newsletter': self.newsletter})
+               return self.issue_archive_page.render_to_response(node, request, extra_context=context)
\ No newline at end of file
diff --git a/models.py b/models.py
deleted file mode 100644 (file)
index 92f713f..0000000
--- a/models.py
+++ /dev/null
@@ -1,417 +0,0 @@
-# encoding: utf-8
-from django.utils.translation import ugettext_lazy as _
-from django.contrib.auth.models import User, Group
-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 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
-from django.utils import simplejson as json
-from UserDict import DictMixin
-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):
-       pass
-
-
-def unregister_value_model(model):
-       pass
-
-
-class Attribute(models.Model):
-       entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
-       entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
-       entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
-       key = models.CharField(max_length=255)
-       json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
-       
-       def get_value(self):
-               return json.loads(self.json_value)
-       
-       def set_value(self, value):
-               self.json_value = json.dumps(value)
-       
-       def delete_value(self):
-               self.json_value = json.dumps(None)
-       
-       value = property(get_value, set_value, delete_value)
-       
-       def __unicode__(self):
-               return u'"%s": %s' % (self.key, self.value)
-
-
-class Relationship(models.Model):
-       entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
-       entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
-       entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
-       key = models.CharField(max_length=255)
-       value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', verbose_name='Value type')
-       value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
-       value = generic.GenericForeignKey('value_content_type', 'value_object_id')
-       
-       def __unicode__(self):
-               return u'"%s": %s' % (self.key, self.value)
-
-
-class QuerySetMapper(object, DictMixin):
-       def __init__(self, queryset, passthrough=None):
-               self.queryset = queryset
-               self.passthrough = passthrough
-       def __getitem__(self, key):
-               try:
-                       return self.queryset.get(key__exact=key).value
-               except ObjectDoesNotExist:
-                       if self.passthrough:
-                               return self.passthrough.__getitem__(key)
-                       raise KeyError
-       def keys(self):
-               keys = set(self.queryset.values_list('key', flat=True).distinct())
-               if self.passthrough:
-                       keys += set(self.passthrough.keys())
-               return list(keys)
-
-
-class Entity(models.Model):
-       attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
-       relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
-       
-       @property
-       def attributes(self):
-               return QuerySetMapper(self.attribute_set)
-       
-       @property
-       def relationships(self):
-               return QuerySetMapper(self.relationship_set)
-       
-       class Meta:
-               abstract = True
-
-
-class Collection(models.Model):
-       name = models.CharField(max_length=255)
-       description = models.TextField(blank=True, null=True)
-       
-       @fattr(short_description='Members')
-       def get_count(self):
-               return self.members.count()
-       
-       def __unicode__(self):
-               return self.name
-
-
-class CollectionMemberManager(models.Manager):
-       use_for_related_fields = True
-
-       def with_model(self, model):
-               return model._default_manager.filter(pk__in=self.filter(member_content_type=ContentType.objects.get_for_model(model)).values_list('member_object_id', flat=True))
-
-
-class CollectionMember(models.Model):
-       objects = CollectionMemberManager()
-       collection = models.ForeignKey(Collection, related_name='members')
-       index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
-       member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
-       member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
-       member = generic.GenericForeignKey('member_content_type', 'member_object_id')
-       
-       def __unicode__(self):
-               return u'%s - %s' % (self.collection, self.member)
-
-
-class TreeManager(models.Manager):
-       use_for_related_fields = True
-       
-       def roots(self):
-               return self.filter(parent__isnull=True)
-       
-       def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
-               """
-               Returns the object with the path, or None if there is no object with that path,
-               unless absolute_result is set to False, in which case it returns a tuple containing
-               the deepest object found along the path, and the remainder of the path after that
-               object as a string (or None in the case that there is no remaining path).
-               """
-               slugs = path.split(pathsep)
-               obj = root
-               remaining_slugs = list(slugs)
-               remainder = None
-               for slug in slugs:
-                       remaining_slugs.remove(slug)
-                       if slug: # ignore blank slugs, handles for multiple consecutive pathseps
-                               try:
-                                       obj = self.get(slug__exact=slug, parent__exact=obj)
-                               except self.model.DoesNotExist:
-                                       if absolute_result:
-                                               obj = None
-                                       remaining_slugs.insert(0, slug)
-                                       remainder = pathsep.join(remaining_slugs)
-                                       break
-               if obj:
-                       if absolute_result:
-                               return obj
-                       else:
-                               return (obj, remainder)
-               raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
-
-
-class TreeModel(models.Model):
-       objects = TreeManager()
-       parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
-       slug = models.SlugField()
-       
-       def get_path(self, pathsep='/', field='slug'):
-               path = getattr(self, field, '?')
-               parent = self.parent
-               while parent:
-                       path = getattr(parent, field, '?') + pathsep + path
-                       parent = parent.parent
-               return path
-       path = property(get_path)
-       
-       def __unicode__(self):
-               return self.path
-       
-       class Meta:
-               abstract = True
-
-
-class TreeEntity(TreeModel, Entity):
-       @property
-       def attributes(self):
-               if self.parent:
-                       return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
-               return super(TreeEntity, self).attributes
-       
-       @property
-       def relationships(self):
-               if self.parent:
-                       return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
-               return super(TreeEntity, self).relationships
-       
-       class Meta:
-               abstract = True
-
-
-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
-               while parent:
-                       path = getattr(parent.instance, field, '?') + pathsep + path
-                       parent = parent.parent
-               return path
-       path = property(get_path)
-       
-       @property
-       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):
-               return HttpResponseServerError()
-               
-       class Meta:
-               unique_together = (('parent', 'slug'),)
-
-
-class MultiNode(Node):
-       accepts_subpath = True
-       
-       urlpatterns = []
-       
-       def render_to_response(self, request, path=None, subpath=None):
-               if not subpath:
-                       subpath = ""
-               subpath = "/" + subpath
-               from django.core.urlresolvers import resolve
-               view, args, kwargs = resolve(subpath, urlconf=self)
-               return view(request, *args, **kwargs)
-       
-       class Meta:
-               abstract = True
-
-
-class Redirect(Node):
-       STATUS_CODES = (
-               (302, 'Temporary'),
-               (301, 'Permanent'),
-       )
-       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):
-               response = HttpResponseRedirect(self.target)
-               response.status_code = self.status_code
-               return response
-
-
-class File(Node):
-       """ For storing arbitrary files """
-       mimetype = models.CharField(max_length=255)
-       file = models.FileField(upload_to='philo/files/%Y/%m/%d')
-       
-       def render_to_response(self, request, path=None, subpath=None):
-               wrapper = FileWrapper(self.file)
-               response = HttpResponse(wrapper, content_type=self.mimetype)
-               response['Content-Length'] = self.file.size
-               return response
-       
-#      def __unicode__(self):
-#              return self.file
-
-
-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, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
-       code = models.TextField(verbose_name='django template code')
-       
-       @property
-       def origin(self):
-               return 'philo.models.Template: ' + self.path
-       
-       @property
-       def django_template(self):
-               return DjangoTemplate(self.code)
-       
-       @property
-       def containers(self):
-               """
-               Returns a tuple where the first item is a list of names of contentlets referenced by containers,
-               and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
-               This will break if there is a recursive extends or includes in the template code.
-               Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
-               """
-               def container_nodes(template):
-                       def nodelist_container_nodes(nodelist):
-                               nodes = []
-                               for node in nodelist:
-                                       try:
-                                               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):
-                                                       nodes.append(node)
-                                               elif isinstance(node, ExtendsNode):
-                                                       extended_template = node.get_parent(Context())
-                                                       if extended_template:
-                                                               nodes.extend(container_nodes(extended_template))
-                                               elif isinstance(node, ConstantIncludeNode):
-                                                       included_template = node.template
-                                                       if included_template:
-                                                               nodes.extend(container_nodes(included_template))
-                                               elif isinstance(node, IncludeNode):
-                                                       included_template = get_template(node.template_name.resolve(Context()))
-                                                       if included_template:
-                                                               nodes.extend(container_nodes(included_template))
-                                       except:
-                                               pass # fail for this node
-                               return nodes
-                       return nodelist_container_nodes(template.nodelist)
-               all_nodes = container_nodes(self.django_template)
-               contentlet_node_names = set([node.name for node in all_nodes if not node.references])
-               contentreference_node_names = []
-               contentreference_node_specs = []
-               for node in all_nodes:
-                       if node.references and node.name not in contentreference_node_names:
-                               contentreference_node_specs.append((node.name, node.references))
-                               contentreference_node_names.append(node.name)
-               return contentlet_node_names, contentreference_node_specs
-       
-       def __unicode__(self):
-               return self.get_path(u' › ', 'name')
-       
-       @staticmethod
-       @fattr(is_usable=True)
-       def loader(template_name, template_dirs=None): # load_template_source
-               try:
-                       template = Template.objects.get_with_path(template_name)
-               except Template.DoesNotExist:
-                       raise TemplateDoesNotExist(template_name)
-               return (template.code, template.origin)
-
-
-class Page(Node):
-       """
-       Represents a page - something which is rendered according to a template. 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)
-       
-       def render_to_response(self, request, path=None, subpath=None):
-               return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
-       
-       def __unicode__(self):
-               return self.get_path(u' › ', 'title')
-
-
-# the following line enables the selection of a node as the root for a given django.contrib.sites Site object
-models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
-
-
-class Contentlet(models.Model):
-       page = models.ForeignKey(Page, related_name='contentlets')
-       name = models.CharField(max_length=255)
-       content = models.TextField()
-       dynamic = models.BooleanField(default=False)
-       
-       def __unicode__(self):
-               return self.name
-
-
-class ContentReference(models.Model):
-       page = models.ForeignKey(Page, related_name='contentreferences')
-       name = models.CharField(max_length=255)
-       content_type = models.ForeignKey(ContentType, verbose_name='Content type')
-       content_id = models.PositiveIntegerField(verbose_name='Content ID')
-       content = generic.GenericForeignKey('content_type', 'content_id')
-       
-       def __unicode__(self):
-               return self.name
-
-
-register_templatetags('philo.templatetags.containers')
-
-
-register_value_model(User)
-register_value_model(Group)
-register_value_model(Site)
-register_value_model(Collection)
-register_value_model(Template)
-register_value_model(Page)
diff --git a/models/__init__.py b/models/__init__.py
new file mode 100644 (file)
index 0000000..b9ea3ac
--- /dev/null
@@ -0,0 +1,11 @@
+from philo.models.base import *
+from philo.models.collections import *
+from philo.models.nodes import *
+from philo.models.pages import *
+from django.contrib.auth.models import User, Group
+from django.contrib.sites.models import Site
+
+
+register_value_model(User)
+register_value_model(Group)
+register_value_model(Site)
\ No newline at end of file
diff --git a/models/base.py b/models/base.py
new file mode 100644 (file)
index 0000000..700b1e7
--- /dev/null
@@ -0,0 +1,193 @@
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from django.utils import simplejson as json
+from django.core.exceptions import ObjectDoesNotExist
+from philo.utils import ContentTypeRegistryLimiter
+from UserDict import DictMixin
+
+
+class Tag(models.Model):
+       name = models.CharField(max_length=250)
+       slug = models.SlugField(unique=True)
+       
+       def __unicode__(self):
+               return self.name
+       
+       class Meta:
+               app_label = 'philo'
+
+
+class Titled(models.Model):
+       title = models.CharField(max_length=255)
+       slug = models.SlugField()
+       
+       def __unicode__(self):
+               return self.title
+       
+       class Meta:
+               abstract = True
+
+
+class Attribute(models.Model):
+       entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
+       entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
+       entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
+       key = models.CharField(max_length=255)
+       json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
+       
+       def get_value(self):
+               return json.loads(self.json_value)
+       
+       def set_value(self, value):
+               self.json_value = json.dumps(value)
+       
+       def delete_value(self):
+               self.json_value = json.dumps(None)
+       
+       value = property(get_value, set_value, delete_value)
+       
+       def __unicode__(self):
+               return u'"%s": %s' % (self.key, self.value)
+       
+       class Meta:
+               app_label = 'philo'
+
+
+value_content_type_limiter = ContentTypeRegistryLimiter()
+
+
+def register_value_model(model):
+       value_content_type_limiter.register_class(model)
+
+
+def unregister_value_model(model):
+       value_content_type_limiter.unregister_class(model)
+
+
+
+class Relationship(models.Model):
+       entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
+       entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
+       entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
+       key = models.CharField(max_length=255)
+       value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type')
+       value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
+       value = generic.GenericForeignKey('value_content_type', 'value_object_id')
+       
+       def __unicode__(self):
+               return u'"%s": %s' % (self.key, self.value)
+       
+       class Meta:
+               app_label = 'philo'
+
+
+class QuerySetMapper(object, DictMixin):
+       def __init__(self, queryset, passthrough=None):
+               self.queryset = queryset
+               self.passthrough = passthrough
+       
+       def __getitem__(self, key):
+               try:
+                       return self.queryset.get(key__exact=key).value
+               except ObjectDoesNotExist:
+                       if self.passthrough is not None:
+                               return self.passthrough.__getitem__(key)
+                       raise KeyError
+       
+       def keys(self):
+               keys = set(self.queryset.values_list('key', flat=True).distinct())
+               if self.passthrough is not None:
+                       keys |= set(self.passthrough.keys())
+               return list(keys)
+
+
+class Entity(models.Model):
+       attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
+       relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
+       
+       @property
+       def attributes(self):
+               return QuerySetMapper(self.attribute_set)
+       
+       @property
+       def relationships(self):
+               return QuerySetMapper(self.relationship_set)
+       
+       class Meta:
+               abstract = True
+
+
+class TreeManager(models.Manager):
+       use_for_related_fields = True
+       
+       def roots(self):
+               return self.filter(parent__isnull=True)
+       
+       def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
+               """
+               Returns the object with the path, or None if there is no object with that path,
+               unless absolute_result is set to False, in which case it returns a tuple containing
+               the deepest object found along the path, and the remainder of the path after that
+               object as a string (or None in the case that there is no remaining path).
+               """
+               slugs = path.split(pathsep)
+               obj = root
+               remaining_slugs = list(slugs)
+               remainder = None
+               for slug in slugs:
+                       remaining_slugs.remove(slug)
+                       if slug: # ignore blank slugs, handles for multiple consecutive pathseps
+                               try:
+                                       obj = self.get(slug__exact=slug, parent__exact=obj)
+                               except self.model.DoesNotExist:
+                                       if absolute_result:
+                                               obj = None
+                                       remaining_slugs.insert(0, slug)
+                                       remainder = pathsep.join(remaining_slugs)
+                                       break
+               if obj:
+                       if absolute_result:
+                               return obj
+                       else:
+                               return (obj, remainder)
+               raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
+
+
+class TreeModel(models.Model):
+       objects = TreeManager()
+       parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
+       slug = models.SlugField()
+       
+       def get_path(self, pathsep='/', field='slug'):
+               path = getattr(self, field, '?')
+               parent = self.parent
+               while parent:
+                       path = getattr(parent, field, '?') + pathsep + path
+                       parent = parent.parent
+               return path
+       path = property(get_path)
+       
+       def __unicode__(self):
+               return self.path
+       
+       class Meta:
+               unique_together = (('parent', 'slug'),)
+               abstract = True
+
+
+class TreeEntity(TreeModel, Entity):
+       @property
+       def attributes(self):
+               if self.parent:
+                       return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
+               return super(TreeEntity, self).attributes
+       
+       @property
+       def relationships(self):
+               if self.parent:
+                       return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
+               return super(TreeEntity, self).relationships
+       
+       class Meta:
+               abstract = True
\ No newline at end of file
diff --git a/models/collections.py b/models/collections.py
new file mode 100644 (file)
index 0000000..9a737eb
--- /dev/null
@@ -0,0 +1,45 @@
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from philo.models.base import value_content_type_limiter, register_value_model
+from philo.utils import fattr
+
+
+class Collection(models.Model):
+       name = models.CharField(max_length=255)
+       description = models.TextField(blank=True, null=True)
+       
+       @fattr(short_description='Members')
+       def get_count(self):
+               return self.members.count()
+       
+       def __unicode__(self):
+               return self.name
+       
+       class Meta:
+               app_label = 'philo'
+
+
+class CollectionMemberManager(models.Manager):
+       use_for_related_fields = True
+
+       def with_model(self, model):
+               return model._default_manager.filter(pk__in=self.filter(member_content_type=ContentType.objects.get_for_model(model)).values_list('member_object_id', flat=True))
+
+
+class CollectionMember(models.Model):
+       objects = CollectionMemberManager()
+       collection = models.ForeignKey(Collection, related_name='members')
+       index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
+       member_content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Member type')
+       member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
+       member = generic.GenericForeignKey('member_content_type', 'member_object_id')
+       
+       def __unicode__(self):
+               return u'%s - %s' % (self.collection, self.member)
+       
+       class Meta:
+               app_label = 'philo'
+
+
+register_value_model(Collection)
\ No newline at end of file
diff --git a/models/nodes.py b/models/nodes.py
new file mode 100644 (file)
index 0000000..a8125ee
--- /dev/null
@@ -0,0 +1,111 @@
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from django.contrib.sites.models import Site
+from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect
+from django.core.servers.basehttp import FileWrapper
+from django.core.urlresolvers import resolve
+from inspect import getargspec
+from philo.models.base import TreeEntity, Entity, QuerySetMapper
+from philo.utils import ContentTypeSubclassLimiter
+from philo.validators import RedirectValidator
+
+
+_view_content_type_limiter = ContentTypeSubclassLimiter(None)
+
+
+class Node(TreeEntity):
+       view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter)
+       view_object_id = models.PositiveIntegerField()
+       view = generic.GenericForeignKey('view_content_type', 'view_object_id')
+       
+       @property
+       def accepts_subpath(self):
+               return self.view.accepts_subpath
+       
+       def render_to_response(self, request, path=None, subpath=None, extra_context=None):
+               return self.view.render_to_response(self, request, path, subpath, extra_context)
+       
+       class Meta:
+               app_label = 'philo'
+
+
+# the following line enables the selection of a node as the root for a given django.contrib.sites Site object
+models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
+
+
+class View(Entity):
+       nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id')
+       
+       accepts_subpath = False
+       
+       def attributes_with_node(self, node):
+               return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
+       
+       def relationships_with_node(self, node):
+               return QuerySetMapper(self.relationship_set, passthrough=node.relationships)
+       
+       def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+               raise NotImplementedError('View subclasses must implement render_to_response.')
+       
+       class Meta:
+               abstract = True
+
+
+_view_content_type_limiter.cls = View
+
+
+class MultiView(View):
+       accepts_subpath = True
+       
+       urlpatterns = []
+       
+       def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+               if not subpath:
+                       subpath = ""
+               subpath = "/" + subpath
+               view, args, kwargs = resolve(subpath, urlconf=self)
+               view_args = getargspec(view)[0]
+               if extra_context is not None and 'extra_context' in view_args:
+                       if 'extra_context' in kwargs:
+                               extra_context.update(kwargs['extra_context'])
+                       kwargs['extra_context'] = extra_context
+               if 'node' in view_args:
+                       kwargs['node'] = node
+               return view(request, *args, **kwargs)
+       
+       class Meta:
+               abstract = True
+
+
+class Redirect(View):
+       STATUS_CODES = (
+               (302, 'Temporary'),
+               (301, 'Permanent'),
+       )
+       target = models.CharField(max_length=200, validators=[RedirectValidator()])
+       status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
+       
+       def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+               response = HttpResponseRedirect(self.target)
+               response.status_code = self.status_code
+               return response
+       
+       class Meta:
+               app_label = 'philo'
+
+
+class File(View):
+       """ For storing arbitrary files """
+       
+       mimetype = models.CharField(max_length=255)
+       file = models.FileField(upload_to='philo/files/%Y/%m/%d')
+       
+       def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+               wrapper = FileWrapper(self.file)
+               response = HttpResponse(wrapper, content_type=self.mimetype)
+               response['Content-Length'] = self.file.size
+               return response
+       
+       class Meta:
+               app_label = 'philo'
\ No newline at end of file
diff --git a/models/pages.py b/models/pages.py
new file mode 100644 (file)
index 0000000..5f75494
--- /dev/null
@@ -0,0 +1,145 @@
+# encoding: utf-8
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from django.conf import settings
+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.template.loader import get_template
+from django.template.loader_tags import ExtendsNode, ConstantIncludeNode, IncludeNode
+from django.http import HttpResponse
+from philo.models.base import TreeModel, register_value_model
+from philo.models.nodes import View
+from philo.utils import fattr
+from philo.templatetags.containers import ContainerNode
+
+
+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, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
+       code = models.TextField(verbose_name='django template code')
+       
+       @property
+       def origin(self):
+               return 'philo.models.Template: ' + self.path
+       
+       @property
+       def django_template(self):
+               return DjangoTemplate(self.code)
+       
+       @property
+       def containers(self):
+               """
+               Returns a tuple where the first item is a list of names of contentlets referenced by containers,
+               and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
+               This will break if there is a recursive extends or includes in the template code.
+               Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
+               """
+               def container_nodes(template):
+                       def nodelist_container_nodes(nodelist):
+                               nodes = []
+                               for node in nodelist:
+                                       try:
+                                               if hasattr(node, 'child_nodelists'):
+                                                       for nodelist_name in node.child_nodelists:
+                                                               if hasattr(node, nodelist_name):
+                                                                       nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
+                                               if isinstance(node, ContainerNode):
+                                                       nodes.append(node)
+                                               elif isinstance(node, ExtendsNode):
+                                                       extended_template = node.get_parent(Context())
+                                                       if extended_template:
+                                                               nodes.extend(container_nodes(extended_template))
+                                               elif isinstance(node, ConstantIncludeNode):
+                                                       included_template = node.template
+                                                       if included_template:
+                                                               nodes.extend(container_nodes(included_template))
+                                               elif isinstance(node, IncludeNode):
+                                                       included_template = get_template(node.template_name.resolve(Context()))
+                                                       if included_template:
+                                                               nodes.extend(container_nodes(included_template))
+                                       except:
+                                               raise # fail for this node
+                               return nodes
+                       return nodelist_container_nodes(template.nodelist)
+               all_nodes = container_nodes(self.django_template)
+               contentlet_node_names = set([node.name for node in all_nodes if not node.references])
+               contentreference_node_names = []
+               contentreference_node_specs = []
+               for node in all_nodes:
+                       if node.references and node.name not in contentreference_node_names:
+                               contentreference_node_specs.append((node.name, node.references))
+                               contentreference_node_names.append(node.name)
+               return contentlet_node_names, contentreference_node_specs
+       
+       def __unicode__(self):
+               return self.get_path(u' › ', 'name')
+       
+       @staticmethod
+       @fattr(is_usable=True)
+       def loader(template_name, template_dirs=None): # load_template_source
+               try:
+                       template = Template.objects.get_with_path(template_name)
+               except Template.DoesNotExist:
+                       raise TemplateDoesNotExist(template_name)
+               return (template.code, template.origin)
+       
+       class Meta:
+               app_label = 'philo'
+
+
+class Page(View):
+       """
+       Represents a page - something which is rendered according to a template. 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)
+       
+       def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+               context = {}
+               context.update(extra_context or {})
+               context.update({'page': self, 'attributes': self.attributes_with_node(node), 'relationships': self.relationships_with_node(node)})
+               return HttpResponse(self.template.django_template.render(RequestContext(request, context)), mimetype=self.template.mimetype)
+       
+       def __unicode__(self):
+               return self.title
+       
+       class Meta:
+               app_label = 'philo'
+
+
+class Contentlet(models.Model):
+       page = models.ForeignKey(Page, related_name='contentlets')
+       name = models.CharField(max_length=255)
+       content = models.TextField()
+       dynamic = models.BooleanField(default=False)
+       
+       def __unicode__(self):
+               return self.name
+       
+       class Meta:
+               app_label = 'philo'
+
+
+class ContentReference(models.Model):
+       page = models.ForeignKey(Page, related_name='contentreferences')
+       name = models.CharField(max_length=255)
+       content_type = models.ForeignKey(ContentType, verbose_name='Content type')
+       content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
+       content = generic.GenericForeignKey('content_type', 'content_id')
+       
+       def __unicode__(self):
+               return self.name
+       
+       class Meta:
+               app_label = 'philo'
+
+
+register_templatetags('philo.templatetags.containers')
+
+
+register_value_model(Template)
+register_value_model(Page)
\ No newline at end of file
index ed8c54e..8b73293 100644 (file)
@@ -19,8 +19,8 @@ class MembersofNode(template.Node):
                except:
                        pass
                return settings.TEMPLATE_STRING_IF_INVALID
-       
-       
+
+
 def do_membersof(parser, token):
        """
        {% membersof <collection> with <model> as <var> %}
diff --git a/urls.py b/urls.py
index c4fcb5e..47be7da 100644 (file)
--- a/urls.py
+++ b/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls.defaults import url, include, patterns, handler404, handler500
+from django.conf.urls.defaults import patterns, url
 from philo.views import node_view
 
 
index e3d1124..340e9e4 100644 (file)
--- a/utils.py
+++ b/utils.py
@@ -1,3 +1,61 @@
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+
+
+class ContentTypeLimiter(object):
+       def q_object(self):
+               return models.Q(pk__in=[])
+       
+       def add_to_query(self, query, *args, **kwargs):
+               query.add_q(self.q_object(), *args, **kwargs)
+
+
+class ContentTypeRegistryLimiter(ContentTypeLimiter):
+       def __init__(self):
+               self.classes = []
+       
+       def register_class(self, cls):
+               self.classes.append(cls)
+       
+       def unregister_class(self, cls):
+               self.classes.remove(cls)
+       
+       def q_object(self):
+               contenttype_pks = []
+               for cls in self.classes:
+                       try:
+                               if issubclass(cls, models.Model):
+                                       if not cls._meta.abstract:
+                                               contenttype = ContentType.objects.get_for_model(cls)
+                                               contenttype_pks.append(contenttype.pk)
+                       except:
+                               pass
+               return models.Q(pk__in=contenttype_pks)
+
+
+class ContentTypeSubclassLimiter(ContentTypeLimiter):
+       def __init__(self, cls, inclusive=False):
+               self.cls = cls
+               self.inclusive = inclusive
+       
+       def q_object(self):
+               contenttype_pks = []
+               def handle_subclasses(cls):
+                       for subclass in cls.__subclasses__():
+                               try:
+                                       if issubclass(subclass, models.Model):
+                                               if not subclass._meta.abstract:
+                                                       if not self.inclusive and subclass is self.cls:
+                                                               continue
+                                                       contenttype = ContentType.objects.get_for_model(subclass)
+                                                       contenttype_pks.append(contenttype.pk)
+                                       handle_subclasses(subclass)
+                               except:
+                                       pass
+               handle_subclasses(self.cls)
+               return models.Q(pk__in=contenttype_pks)
+
+
 def fattr(*args, **kwargs):
        def wrapper(function):
                for key in kwargs:
index bc41d02..637dba8 100644 (file)
@@ -1,77 +1,30 @@
-from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext_lazy as _
+from django.core.validators import RegexValidator
+import re
 
 
-class TreeParentValidator(object):
-       """
-       constructor takes instance and parent_attr, where instance is the model
-       being validated and parent_attr is where to look on that parent for the
-       comparison.
-       """
-       #message = _("A tree element can't be its own parent.")
-       code = 'invalid'
-       
-       def __init__(self, instance, parent_attr=None, message=None, code=None):
-               self.instance = instance
-               self.parent_attr = parent_attr
-               self.static_message = message
-               if code is not None:
-                       self.code = code
-       
-       def __call__(self, value):
-               """
-               Validates that the self.instance is not found in the parent tree of
-               the node given as value.
-               """
-               parent = value
-               
-               while parent:
-                       comparison=self.get_comparison(parent)
-                       if comparison == self.instance:
-                               # using (self.message, code=self.code) results in the admin interface
-                               # screwing with the error message and making it be 'Enter a valid value'
-                               raise ValidationError(self.message)
-                       parent=parent.parent
-       
-       def get_comparison(self, parent):
-               if self.parent_attr and hasattr(parent, self.parent_attr):
-                       return getattr(parent, self.parent_attr)
-               
-               return parent
-       
-       def get_message(self):
-               return self.static_message or _(u"A %s can't be its own parent." % self.instance.__class__.__name__)
-       message = property(get_message)
-       
-class TreePositionValidator(object):
-       code = 'invalid'
-       
-       def __init__(self, parent, slug, obj_class, message=None, code=None):
-               self.parent = parent
-               self.slug = slug
-               self.obj_class = obj_class
-               self.static_message = message
-                       
-               if code is not None:
-                       self.code = code
-       
-       def __call__(self, value):
-               """
-               Validates that there is no obj of obj_class with the same position
-               as the compared obj (value) but a different id.
-               """
-               if not isinstance(value, self.obj_class):
-                       raise ValidationError(_(u"The value must be an instance of %s." % self.obj_class.__name__))
-               
-               try:
-                       obj = self.obj_class.objects.get(slug=self.slug, parent=self.parent)
-                       
-                       if obj.id != value.id:
-                               raise ValidationError(self.message)
-                               
-               except self.obj_class.DoesNotExist:
-                       pass
-       
-       def get_message(self):
-               return self.static_message or _(u"A %s with that path (parent and slug) already exists." % self.obj_class.__name__)
-       message = property(get_message)
+class RedirectValidator(RegexValidator):
+       """Based loosely on the URLValidator, but no option to verify_exists"""
+       regex = re.compile(
+               r'^(?:https?://' # http:// or https://
+               r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
+               r'localhost|' #localhost...
+               r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
+               r'(?::\d+)?' # optional port
+               r'(?:/?|[/?#]?\S+)|'
+               r'[^?#\s]\S*)$',
+               re.IGNORECASE)
+       message = _(u'Enter a valid absolute or relative redirect target')
+
+
+class URLLinkValidator(RegexValidator):
+       """Based loosely on the URLValidator, but no option to verify_exists"""
+       regex = re.compile(
+               r'^(?:https?://' # http:// or https://
+               r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
+               r'localhost|' #localhost...
+               r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
+               r'(?::\d+)?' # optional port
+               r'|)' # also allow internal links
+               r'(?:/?|[/?#]?\S+)$', re.IGNORECASE)
+       message = _(u'Enter a valid absolute or relative redirect target')
index f086bfd..911adfe 100644 (file)
--- a/views.py
+++ b/views.py
@@ -17,6 +17,6 @@ def node_view(request, path=None, **kwargs):
                raise Http404
        if not node:
                raise Http404
-       if subpath and not node.instance.accepts_subpath:
+       if subpath and not node.accepts_subpath:
                raise Http404
-       return node.instance.render_to_response(request, path=path, subpath=subpath)
+       return node.render_to_response(request, path=path, subpath=subpath)