Merge git://github.com/melinath/philo into melinath
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Thu, 17 Jun 2010 08:05:44 +0000 (04:05 -0400)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Thu, 17 Jun 2010 08:05:44 +0000 (04:05 -0400)
* git://github.com/melinath/philo:
  Fixed collapse classes
  Added contentlet unicode and fixed container context bug.
  Made referencing containers optional on pages. Fixes a bug.
  Added File unicode.
  Last random entry before more organized approach. Added documentation/admin display information and membersof templatetag. Fixed minor admin/template bugs.
  Reopened treechanges. Moved validation language to validators.py. Abstracted NodeForm to TreeForm.
  Added admin form validation to prevent: 1. a treemodel being its own parent (uses a validate_parents method added to the model.) 2. a node having the same path (i.e. parent+slug) as another Also added a ModelAdmin for the File model.
  Went back and fixed spacing issues with last cleanup. Also added some list display options to the redirect, template and page models to improve the admin site.
  Last random entry before more organized approach. Added documentation/admin display information and membersof templatetag. Fixed minor admin/template bugs.
  Reopened treechanges. Moved validation language to validators.py. Abstracted NodeForm to TreeForm.
  Added admin form validation to prevent: 1. a treemodel being its own parent (uses a validate_parents method added to the model.) 2. a node having the same path (i.e. parent+slug) as another Also added a ModelAdmin for the File model.
  Went back and fixed spacing issues with last cleanup. Also added some list display options to the redirect, template and page models to improve the admin site.

admin.py
models.py
templates/admin/philo/edit_inline/tabular_collapse.html
templatetags/collections.py [new file with mode: 0644]
templatetags/containers.py
validators.py [new file with mode: 0644]

index 178a904..0f34028 100644 (file)
--- a/admin.py
+++ b/admin.py
@@ -8,6 +8,11 @@ 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):
@@ -37,16 +42,73 @@ class CollectionMemberInline(admin.TabularInline):
        fk_name = 'collection'
        model = CollectionMember
        extra = 1
-       classes = ('collapse-closed',)
+       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 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 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
 
 
 class ModelLookupWidget(forms.TextInput):
@@ -76,16 +138,46 @@ class ModelLookupWidget(forms.TextInput):
                return mark_safe(output)
 
 
+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 NodeAdmin(EntityAdmin):
+       pass
+
+
 class RedirectAdmin(NodeAdmin):
        fieldsets = (
                (None, {
                        'fields': ('slug', 'target', 'status_code')
                }),
                ('URL/Tree/Hierarchy', {
-                       'classes': ('collapse', 'collapse-closed'),
+                       'classes': COLLAPSE_CLASSES,
                        'fields': ('parent',)
                }),
        )
+       list_display=('slug', 'target', 'path', 'status_code',)
+       list_filter=('status_code',)
+       form = RedirectAdminForm
 
 
 class FileAdmin(NodeAdmin):
@@ -95,10 +187,12 @@ class FileAdmin(NodeAdmin):
                        'fields': ('file', 'slug', 'mimetype')
                }),
                ('URL/Tree/Hierarchy', {
-                       'classes': ('collapse', 'collapse-closed'),
+                       'classes': COLLAPSE_CLASSES,
                        'fields': ('parent',)
                }),
        )
+       form=FileAdminForm
+       list_display=('slug', 'mimetype', 'path', 'file',)
 
 
 class PageAdmin(NodeAdmin):
@@ -109,13 +203,14 @@ class PageAdmin(NodeAdmin):
                        'fields': ('title', 'slug', 'template')
                }),
                ('URL/Tree/Hierarchy', {
-                       'classes': ('collapse', 'collapse-closed'),
+                       '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)
@@ -178,32 +273,23 @@ class PageAdmin(NodeAdmin):
                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)]
+                               if content == None:
+                                       continue
                                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', 'collapse-closed'),
-                       'fields': ('documentation',)
-               }),
-               (None, {
-                       'fields': ('code',)
-               }),
-               ('Advanced', {
-                       'classes': ('collapse','collapse-closed'),
-                       'fields': ('mimetype',)
-               }),
-       )
-       save_on_top = True
-       save_as = True
+class RedirectAdmin(admin.ModelAdmin):
+       list_display=('slug', 'target', 'path', 'status_code',)
+       list_filter=('status_code',)
+       form = RedirectAdminForm
+
+
+class FileAdmin(admin.ModelAdmin):
+       form=FileAdminForm
+       list_display=('slug', 'mimetype', 'path', 'file',)
 
 
 admin.site.register(Collection, CollectionAdmin)
index ecd1c1f..88863b2 100644 (file)
--- a/models.py
+++ b/models.py
@@ -96,12 +96,18 @@ class Entity(models.Model):
        
        class Meta:
                abstract = True
-
+       
 
 class Collection(models.Model):
        name = models.CharField(max_length=255)
        description = models.TextField(blank=True, null=True)
-
+       
+       def get_count(self):
+               return self.members.count()
+       get_count.short_description = 'Members'
+       
+       def __unicode__(self):
+               return self.name
 
 class CollectionMemberManager(models.Manager):
        use_for_related_fields = True
@@ -117,6 +123,9 @@ class CollectionMember(models.Model):
        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):
@@ -235,6 +244,9 @@ class Node(InheritableTreeEntity):
        
        def render_to_response(self, request, path=None, subpath=None):
                return HttpResponseServerError()
+               
+       class Meta:
+               unique_together=(('parent', 'slug',),)
 
 
 class MultiNode(Node):
@@ -278,12 +290,15 @@ class File(Node):
                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)
+       mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE, default=settings.DEFAULT_CONTENT_TYPE)
        code = models.TextField(verbose_name='django template code')
        
        @property
@@ -353,9 +368,7 @@ class Template(TreeModel):
 
 class Page(Node):
        """
-       Represents an HTML page. The page will have a number of related Contentlets
-       depending on the template selected - but these will appear only after the
-       page has been saved with that template.
+       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)
@@ -376,6 +389,9 @@ class Contentlet(models.Model):
        name = models.CharField(max_length=255)
        content = models.TextField()
        dynamic = models.BooleanField(default=False)
+       
+       def __unicode__(self):
+               return self.name
 
 
 class ContentReference(models.Model):
@@ -384,6 +400,9 @@ class ContentReference(models.Model):
        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')
index c457297..6aecaf5 100644 (file)
@@ -2,7 +2,7 @@
 <div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
   <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
 {{ inline_admin_formset.formset.management_form }}
-<fieldset class="module collapse collapse-closed">
+<fieldset class="module collapse collapse-closed closed">
    <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
    {{ inline_admin_formset.formset.non_form_errors }}
    <table>
diff --git a/templatetags/collections.py b/templatetags/collections.py
new file mode 100644 (file)
index 0000000..ed8c54e
--- /dev/null
@@ -0,0 +1,43 @@
+from django import template
+from django.conf import settings
+
+
+register = template.Library()
+
+
+class MembersofNode(template.Node):
+       def __init__(self, collection, model, as_var):
+               self.collection = template.Variable(collection)
+               self.model = template.Variable(model)
+               self.as_var = as_var
+               
+       def render(self, context):
+               try:
+                       collection = self.collection.resolve(context)
+                       model = self.model.resolve(context)
+                       context[self.as_var] = collection.members.with_model(model)
+               except:
+                       pass
+               return settings.TEMPLATE_STRING_IF_INVALID
+       
+       
+def do_membersof(parser, token):
+       """
+       {% membersof <collection> with <model> as <var> %}
+       """
+       params=token.split_contents()
+       tag = params[0]
+       
+       if len(params) < 6:
+               raise template.TemplateSyntaxError('"%s" template tag requires six parameters' % tag)
+               
+       if params[2] != 'with':
+               raise template.TemplateSyntaxError('"%s" template tag requires the third parameter to be "with"' % tag)
+               
+       if params[4] != 'as':
+               raise template.TemplateSyntaxError('"%s" template tag requires the fifth parameter to be "as"' % tag)
+       
+       return MembersofNode(collection=params[1], model=params[3], as_var=params[5])
+
+
+register.tag('membersof', do_membersof)
\ No newline at end of file
index ca5e1e9..90debf6 100644 (file)
@@ -29,10 +29,9 @@ class ContainerNode(template.Node):
        def render(self, context):
                content = settings.TEMPLATE_STRING_IF_INVALID
                if 'page' in context:
-                       container_content = self.get_container_content(context['page'])
+                       container_content = self.get_container_content(context)
                
-               if self.nodelist_main is None:
-                       self.nodelist_main
+               if not self.nodelist_main:
                        if container_content and self.as_var:
                                context[self.as_var] = container_content
                                return ''
@@ -56,7 +55,8 @@ class ContainerNode(template.Node):
                
                return ''
        
-       def get_container_content(self, page):
+       def get_container_content(self, context):
+               page = context['page']
                if self.references:
                        try:
                                contentreference = page.contentreferences.get(name__exact=self.name, content_type=self.references)
@@ -71,7 +71,7 @@ class ContainerNode(template.Node):
                                                content = mark_safe(template.Template(contentlet.content, name=contentlet.name).render(context))
                                        except template.TemplateSyntaxError, error:
                                                if settings.DEBUG:
-                                                       content = ('[Error parsing contentlet \'%s\': %s]' % self.name, error)
+                                                       content = ('[Error parsing contentlet \'%s\': %s]' % (self.name, error))
                                else:
                                        content = contentlet.content
                        except ObjectDoesNotExist:
diff --git a/validators.py b/validators.py
new file mode 100644 (file)
index 0000000..bc41d02
--- /dev/null
@@ -0,0 +1,77 @@
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+
+
+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)