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/>.
-from philo.models import Template
+from philo.models.pages import Template
load_template_source = Template.loader
+++ /dev/null
-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 += ' <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)
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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 += ' <strong>%s</strong>' % escape(truncate_words(value_object, 14))
+ except value_class.DoesNotExist:
+ pass
+ return mark_safe(output)
\ No newline at end of file
-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):
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
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'),
('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')
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)
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)
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
+++ /dev/null
-# 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)
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+# 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
except:
pass
return settings.TEMPLATE_STRING_IF_INVALID
-
-
+
+
def do_membersof(parser, token):
"""
{% membersof <collection> with <model> as <var> %}
-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
+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:
-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')
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)