From: Kriti Godey Date: Fri, 2 Jul 2010 11:51:13 +0000 (+0530) Subject: Merge branch 'master' into penfield, cleaned up. X-Git-Tag: philo-0.9~59^2^2 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/4b094dbc60c3853814c2523d5772e4cf2884a073?hp=ee24ab2feac614a0bdc7114e8e2b4b4d0ad44395 Merge branch 'master' into penfield, cleaned up. --- diff --git a/README b/README index 4170400..bf55344 100644 --- a/README +++ b/README @@ -2,7 +2,7 @@ Philo is a foundation for developing web content management systems. Prerequisites: * Python 2.5.4+ - * Django 1.1.1+ + * Django 1.2+ * (Optional) django-grappelli 2.0+ To contribute, please visit the project website . diff --git a/__init__.py b/__init__.py index 9fc18ae..52956f3 100644 --- a/__init__.py +++ b/__init__.py @@ -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 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 += '' % (related_url, name) - output += '%s' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')) - output += '' - if value: - value_class = self.content_type.model_class() - try: - value_object = value_class.objects.get(pk=value) - output += ' %s' % 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 index 0000000..9039448 --- /dev/null +++ b/admin/__init__.py @@ -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 index 0000000..bdf9f38 --- /dev/null +++ b/admin/base.py @@ -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 index 0000000..dfc4826 --- /dev/null +++ b/admin/collections.py @@ -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 index 0000000..093537e --- /dev/null +++ b/admin/nodes.py @@ -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 index 0000000..fc60ad1 --- /dev/null +++ b/admin/pages.py @@ -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 index 0000000..f8799fe --- /dev/null +++ b/admin/widgets.py @@ -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 += '' % (related_url, name) + output += '%s' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')) + output += '' + if value: + value_class = self.content_type.model_class() + try: + value_object = value_class.objects.get(pk=value) + output += ' %s' % escape(truncate_words(value_object, 14)) + except value_class.DoesNotExist: + pass + return mark_safe(output) \ No newline at end of file diff --git a/contrib/penfield/admin.py b/contrib/penfield/admin.py index 0e15287..ba885fd 100644 --- a/contrib/penfield/admin.py +++ b/contrib/penfield/admin.py @@ -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 diff --git a/contrib/penfield/models.py b/contrib/penfield/models.py index a5ba85a..8d7e3ff 100644 --- a/contrib/penfield/models.py +++ b/contrib/penfield/models.py @@ -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>[-\w]+)/?' % self.tag_permalink_base), self.tag_view) + url((r'^(?:%s)/?$' % self.tag_permalink_base), self.tag_archive_view), + url((r'^(?:%s)/(?P[-\w]+)/?$' % self.tag_permalink_base), self.tag_view) ) if self.entry_permalink_style == 'D': entry_patterns = patterns('', - url(r'^(?P\d{4})/?$', self.archive_view), - url(r'^(?P\d{4})/(?P\d{2})/?$', self.archive_view), - url(r'^(?P\d{4})/(?P\d{2})/(?P\d+)/?$', self.archive_view), - url(r'^(?P\d{4})/(?P\d{2})/(?P\d+)/(?P[-\w]+)/?', self.entry_view) + url(r'^(?P\d{4})/?$', self.entry_archive_view), + url(r'^(?P\d{4})/(?P\d{2})/?$', self.entry_archive_view), + url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/?$', self.entry_archive_view), + url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[-\w]+)/?$', self.entry_view) ) elif self.entry_permalink_style == 'M': entry_patterns = patterns('', - url(r'^(?P\d{4})/?$', self.archive_view), - url(r'^(?P\d{4})/(?P\d{2})/?$', self.archive_view), - url(r'^(?P\d{4})/(?P\d{2})/(?P>[-\w]+)/?', self.entry_view) + url(r'^(?P\d{4})/?$', self.entry_archive_view), + url(r'^(?P\d{4})/(?P\d{2})/?$', self.entry_archive_view), + url(r'^(?P\d{4})/(?P\d{2})/(?P[-\w]+)/?$', self.entry_view) ) elif self.entry_permalink_style == 'Y': entry_patterns = patterns('', - url(r'^(?P\d{4})/?$', self.archive_view), - url(r'^(?P\d{4})/(?P>[-\w]+)/?', self.entry_view) + url(r'^(?P\d{4})/?$', self.entry_archive_view), + url(r'^(?P\d{4})/(?P[-\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>[-\w]+)/?' % self.entry_permalink_base), self.entry_view) + url((r'^(?:%s)/?$' % self.entry_permalink_base), self.entry_archive_view), + url((r'^(?:%s)/(?P[-\w]+)/?$' % self.entry_permalink_base), self.entry_view) ) else: entry_patterns = patterns('', - url(r'^(?P>[-\w]+)/?', self.entry_view) + url(r'^(?P[-\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\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\d{4})/?$' % self.article_permalink_base), self.article_archive_view) + ) + if self.article_permalink_style in 'DM': + article_patterns += patterns('', + url((r'^(?:%s)/(?P\d{4})/(?P\d{2})/?$' % self.article_permalink_base), self.article_archive_view) + ) + if self.article_permalink_style == 'D': + article_patterns += patterns('', + url((r'^(?:%s)/(?P\d{4})/(?P\d{2})/(?P\d{2})/?$' % self.article_permalink_base), self.article_archive_view), + url((r'^(?:%s)/(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[-\w]+)/?$' % self.article_permalink_base), self.article_view) + ) + else: + article_patterns += patterns('', + url((r'^(?:%s)/(?P\d{4})/(?P\d{2})/(?P[-\w]+)/?$' % self.article_permalink_base), self.article_view) + ) + else: + article_patterns += patterns('', + url((r'^(?:%s)/(?P\d{4})/(?P[-\w]+)/?$' % self.article_permalink_base), self.article_view) + ) + else: + article_patterns += patterns('', + url((r'^(?:%s)/(?P[-\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 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 index 0000000..b9ea3ac --- /dev/null +++ b/models/__init__.py @@ -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 index 0000000..700b1e7 --- /dev/null +++ b/models/base.py @@ -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 index 0000000..9a737eb --- /dev/null +++ b/models/collections.py @@ -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 index 0000000..a8125ee --- /dev/null +++ b/models/nodes.py @@ -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 index 0000000..5f75494 --- /dev/null +++ b/models/pages.py @@ -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 diff --git a/templatetags/collections.py b/templatetags/collections.py index ed8c54e..8b73293 100644 --- a/templatetags/collections.py +++ b/templatetags/collections.py @@ -19,8 +19,8 @@ class MembersofNode(template.Node): except: pass return settings.TEMPLATE_STRING_IF_INVALID - - + + def do_membersof(parser, token): """ {% membersof with as %} diff --git a/urls.py b/urls.py index c4fcb5e..47be7da 100644 --- 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 diff --git a/utils.py b/utils.py index e3d1124..340e9e4 100644 --- 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: diff --git a/validators.py b/validators.py index bc41d02..637dba8 100644 --- a/validators.py +++ b/validators.py @@ -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') diff --git a/views.py b/views.py index f086bfd..911adfe 100644 --- 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)