From 46fdca9049d4b7806bc4d6ec33973a3fc19153c4 Mon Sep 17 00:00:00 2001 From: Joseph Spiros Date: Fri, 25 Jun 2010 13:58:04 -0400 Subject: [PATCH] Initial implementation of new Node system whereby Nodes are separate from the objects that render them. --- admin.py | 293 ------------------------------------ admin/__init__.py | 4 + admin/base.py | 39 +++++ admin/collections.py | 20 +++ admin/nodes.py | 35 +++++ admin/pages.py | 118 +++++++++++++++ admin/widgets.py | 29 ++++ models/base.py | 65 ++------ models/collections.py | 4 +- models/nodes.py | 54 +++++-- models/pages.py | 8 +- templatetags/collections.py | 4 +- urls.py | 2 +- utils.py | 55 +++++++ validators.py | 77 ---------- views.py | 4 +- 16 files changed, 367 insertions(+), 444 deletions(-) delete mode 100644 admin.py create mode 100644 admin/__init__.py create mode 100644 admin/base.py create mode 100644 admin/collections.py create mode 100644 admin/nodes.py create mode 100644 admin/pages.py create mode 100644 admin/widgets.py diff --git a/admin.py b/admin.py deleted file mode 100644 index 9e991d9..0000000 --- a/admin.py +++ /dev/null @@ -1,293 +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, AttributeError): - 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)] - 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',) - 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..b6f47a0 --- /dev/null +++ b/admin/base.py @@ -0,0 +1,39 @@ +from django.contrib import admin +#from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +#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 philo.admin import widgets +#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 \ 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..a1f5bae --- /dev/null +++ b/admin/widgets.py @@ -0,0 +1,29 @@ +from django import forms +from django.conf import settings + + +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/models/base.py b/models/base.py index 5ca9d93..291fe70 100644 --- a/models/base.py +++ b/models/base.py @@ -3,17 +3,10 @@ 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 -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') @@ -39,12 +32,24 @@ class Attribute(models.Model): 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', verbose_name='Value type') + 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') @@ -144,6 +149,7 @@ class TreeModel(models.Model): return self.path class Meta: + unique_together = (('parent', 'slug'),) abstract = True app_label = 'philo' @@ -161,47 +167,6 @@ class TreeEntity(TreeModel, Entity): return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships) return super(TreeEntity, self).relationships - class Meta: - abstract = True - app_label = 'philo' - - -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): - try: - return self.instance_type.get_object_for_this_type(id=self.id) - except: - return None - - def get_path(self, pathsep='/', field='slug'): - path = getattr(self.instance, field, getattr(self.instance, 'slug', '?')) - parent = self.parent - while parent: - path = getattr(parent.instance, field, getattr(parent.instance, 'slug', '?')) + 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 app_label = 'philo' \ No newline at end of file diff --git a/models/collections.py b/models/collections.py index 61b2608..9a737eb 100644 --- a/models/collections.py +++ b/models/collections.py @@ -1,7 +1,7 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic -from philo.models.base import register_value_model +from philo.models.base import value_content_type_limiter, register_value_model from philo.utils import fattr @@ -31,7 +31,7 @@ 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_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') diff --git a/models/nodes.py b/models/nodes.py index 78df3d6..bb1601e 100644 --- a/models/nodes.py +++ b/models/nodes.py @@ -1,19 +1,30 @@ 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 philo.models.base import InheritableTreeEntity +from philo.models.base import TreeEntity, Entity +from philo.utils import ContentTypeSubclassLimiter from philo.validators import RedirectValidator -class Node(InheritableTreeEntity): - accepts_subpath = False +_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): - return HttpResponseServerError() - + return self.view.render_to_response(self, request, path, subpath) + class Meta: - unique_together = (('parent', 'slug'),) app_label = 'philo' @@ -21,12 +32,28 @@ class Node(InheritableTreeEntity): models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node') -class MultiNode(Node): +class View(Entity): + nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id') + + accepts_subpath = False + + def render_to_response(self, node, request, path=None, subpath=None): + raise NotImplementedError('View subclasses must implement render_to_response.') + + class Meta: + abstract = True + app_label = 'philo' + + +_view_content_type_limiter.cls = View + + +class MultiView(View): accepts_subpath = True urlpatterns = [] - def render_to_response(self, request, path=None, subpath=None): + def render_to_response(self, node, request, path=None, subpath=None): if not subpath: subpath = "" subpath = "/" + subpath @@ -39,15 +66,15 @@ class MultiNode(Node): app_label = 'philo' -class Redirect(Node): +class Redirect(View): STATUS_CODES = ( (302, 'Temporary'), (301, 'Permanent'), ) - target = models.CharField(max_length=200,validators=[RedirectValidator()]) + 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, request, path=None, subpath=None): + def render_to_response(self, node, request, path=None, subpath=None): response = HttpResponseRedirect(self.target) response.status_code = self.status_code return response @@ -56,12 +83,13 @@ class Redirect(Node): app_label = 'philo' -class File(Node): +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, request, path=None, subpath=None): + def render_to_response(self, node, request, path=None, subpath=None): wrapper = FileWrapper(self.file) response = HttpResponse(wrapper, content_type=self.mimetype) response['Content-Length'] = self.file.size diff --git a/models/pages.py b/models/pages.py index 2ba56d0..b9e481b 100644 --- a/models/pages.py +++ b/models/pages.py @@ -11,7 +11,7 @@ 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 Node +from philo.models.nodes import View from philo.utils import fattr from philo.templatetags.containers import ContainerNode @@ -90,18 +90,18 @@ class Template(TreeModel): app_label = 'philo' -class Page(Node): +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, request, path=None, subpath=None): + def render_to_response(self, node, 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') + return self.title class Meta: app_label = 'philo' 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..5fc3274 100644 --- a/utils.py +++ b/utils.py @@ -1,3 +1,58 @@ +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 = [] + for subclass in self.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) + except: + pass + 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 0c6fa78..637dba8 100644 --- a/validators.py +++ b/validators.py @@ -1,85 +1,8 @@ -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( 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) -- 2.20.1