from django.contrib.admin.widgets import AdminTextareaWidget
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
-from django.forms.models import ModelForm, BaseInlineFormSet
+from django.forms.models import ModelForm, BaseInlineFormSet, BaseModelFormSet
from django.forms.formsets import TOTAL_FORM_COUNT
+from django.utils.datastructures import SortedDict
from philo.admin.widgets import ModelLookupWidget
from philo.models import Contentlet, ContentReference
def __init__(self, *args, **kwargs):
super(ContainerForm, self).__init__(*args, **kwargs)
self.verbose_name = self.instance.name.replace('_', ' ')
+ self.prefix = self.instance.name
class ContentletForm(ContainerForm):
content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content')
def should_delete(self):
- return not bool(self.cleaned_data['content'])
+ # Delete iff: the data has changed and is now empty.
+ return self.has_changed() and not bool(self.cleaned_data['content'])
class Meta:
model = Contentlet
- fields = ['name', 'content']
+ fields = ['content']
class ContentReferenceForm(ContainerForm):
pass
def should_delete(self):
- return (self.cleaned_data['content_id'] is None)
+ return self.has_changed() and (self.cleaned_data['content_id'] is None)
class Meta:
model = ContentReference
- fields = ['name', 'content_id']
+ fields = ['content_id']
class ContainerInlineFormSet(BaseInlineFormSet):
- def __init__(self, containers, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
- # Unfortunately, I need to add some things to BaseInline between its __init__ and its
- # super call, so a lot of this is repetition.
+ @property
+ def containers(self):
+ if not hasattr(self, '_containers'):
+ self._containers = self.get_containers()
+ return self._containers
+
+ def total_form_count(self):
+ # This ignores the posted management form data... but that doesn't
+ # seem to have any ill side effects.
+ return len(self.containers.keys())
+
+ def _get_initial_forms(self):
+ return [form for form in self.forms if form.instance.pk is not None]
+ initial_forms = property(_get_initial_forms)
+
+ def _get_extra_forms(self):
+ return [form for form in self.forms if form.instance.pk is None]
+ extra_forms = property(_get_extra_forms)
+
+ def _construct_form(self, i, **kwargs):
+ if 'instance' not in kwargs:
+ kwargs['instance'] = self.containers.values()[i]
- # Start cribbed from BaseInline
- from django.db.models.fields.related import RelatedObject
- self.save_as_new = save_as_new
- # is there a better way to get the object descriptor?
- self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
- if self.fk.rel.field_name == self.fk.rel.to._meta.pk.name:
- backlink_value = self.instance
- else:
- backlink_value = getattr(self.instance, self.fk.rel.field_name)
- if queryset is None:
- queryset = self.model._default_manager
- qs = queryset.filter(**{self.fk.name: backlink_value})
- # End cribbed from BaseInline
+ # Skip over the BaseModelFormSet. We have our own way of doing things!
+ form = super(BaseModelFormSet, self)._construct_form(i, **kwargs)
- self.container_instances, qs = self.get_container_instances(containers, qs)
- self.extra_containers = containers
- self.extra = len(self.extra_containers)
- super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs)
-
- def get_container_instances(self, containers, qs):
- raise NotImplementedError
+ # Since we skipped over BaseModelFormSet, we need to duplicate what BaseInlineFormSet would do.
+ if self.save_as_new:
+ # Remove the primary key from the form's data, we are only
+ # creating new instances
+ form.data[form.add_prefix(self._pk_field.name)] = None
+
+ # Remove the foreign key from the form's data
+ form.data[form.add_prefix(self.fk.name)] = None
+
+ # Set the fk value here so that the form can do it's validation.
+ setattr(form.instance, self.fk.get_attname(), self.instance.pk)
+ return form
- def total_form_count(self):
- if self.data or self.files:
- return self.management_form.cleaned_data[TOTAL_FORM_COUNT]
+ def add_fields(self, form, index):
+ """Override the pk field's initial value with a real one."""
+ super(ContainerInlineFormSet, self).add_fields(form, index)
+ if index is not None:
+ pk_value = self.containers.values()[index].pk
else:
- return self.initial_form_count() + self.extra
+ pk_value = None
+ form.fields[self._pk_field.name].initial = pk_value
def save_existing_objects(self, commit=True):
self.changed_objects = []
self.deleted_objects = []
if not self.get_queryset():
return []
-
+
saved_instances = []
for form in self.initial_forms:
pk_name = self._pk_field.name
raw_pk_value = form._raw_value(pk_name)
-
+
# clean() for different types of PK fields can sometimes return
# the model instance, and sometimes the PK. Handle either.
pk_value = form.fields[pk_name].clean(raw_pk_value)
pk_value = getattr(pk_value, 'pk', pk_value)
-
- obj = self._existing_object(pk_value)
- if form.should_delete():
- self.deleted_objects.append(obj)
- obj.delete()
- continue
- if form.has_changed():
- self.changed_objects.append((obj, form.changed_data))
- saved_instances.append(self.save_existing(form, obj, commit=commit))
- if not commit:
- self.saved_forms.append(form)
+
+ # if the pk_value is None, they have just switched to a
+ # template which didn't contain data about this container.
+ # Skip!
+ if pk_value is not None:
+ obj = self._existing_object(pk_value)
+ if form.should_delete():
+ self.deleted_objects.append(obj)
+ obj.delete()
+ continue
+ if form.has_changed():
+ self.changed_objects.append((obj, form.changed_data))
+ saved_instances.append(self.save_existing(form, obj, commit=commit))
+ if not commit:
+ self.saved_forms.append(form)
return saved_instances
def save_new_objects(self, commit=True):
class ContentletInlineFormSet(ContainerInlineFormSet):
- def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
- if instance is None:
- self.instance = self.fk.rel.to()
- else:
- self.instance = instance
-
+ def get_containers(self):
try:
containers = list(self.instance.containers[0])
except ObjectDoesNotExist:
containers = []
-
- super(ContentletInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
-
- def get_container_instances(self, containers, qs):
- qs = qs.filter(name__in=containers)
- container_instances = []
- for container in qs:
- container_instances.append(container)
- containers.remove(container.name)
- return container_instances, qs
-
- def _construct_form(self, i, **kwargs):
- if i >= self.initial_form_count(): # and not kwargs.get('instance'):
- kwargs['instance'] = self.model(name=self.extra_containers[i - self.initial_form_count() - 1])
- return super(ContentletInlineFormSet, self)._construct_form(i, **kwargs)
+ qs = self.get_queryset().filter(name__in=containers)
+ container_dict = SortedDict([(container.name, container) for container in qs])
+ for name in containers:
+ if name not in container_dict:
+ container_dict[name] = self.model(name=name)
+
+ container_dict.keyOrder = containers
+ return container_dict
class ContentReferenceInlineFormSet(ContainerInlineFormSet):
- def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
- if instance is None:
- self.instance = self.fk.rel.to()
- else:
- self.instance = instance
-
+ def get_containers(self):
try:
- containers = list(self.instance.containers[1])
+ containers = self.instance.containers[1]
except ObjectDoesNotExist:
- containers = []
-
- super(ContentReferenceInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
-
- def get_container_instances(self, containers, qs):
- filter = Q()
+ containers = {}
- for name, ct in containers:
+ filter = Q()
+ for name, ct in containers.items():
filter |= Q(name=name, content_type=ct)
+ qs = self.get_queryset().filter(filter)
- qs = qs.filter(filter)
- container_instances = []
- for container in qs:
- container_instances.append(container)
- containers.remove((container.name, container.content_type))
- return container_instances, qs
-
- def _construct_form(self, i, **kwargs):
- if i >= self.initial_form_count(): # and not kwargs.get('instance'):
- name, content_type = self.extra_containers[i - self.initial_form_count() - 1]
- kwargs['instance'] = self.model(name=name, content_type=content_type)
-
- return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs)
\ No newline at end of file
+ container_dict = SortedDict([(container.name, container) for container in qs])
+
+ keyOrder = []
+ for name, ct in containers.items():
+ keyOrder.append(name)
+ if name not in container_dict:
+ container_dict[name] = self.model(name=name, content_type=ct)
+
+ container_dict.keyOrder = keyOrder
+ return container_dict
\ No newline at end of file
-from django.contrib import admin
from django import forms
+from django.contrib import admin
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect, QueryDict
from philo.admin import EntityAdmin, AddTagAdmin, COLLAPSE_CLASSES
from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView
'classes': COLLAPSE_CLASSES
})
)
+ actions = ['make_issue']
def author_names(self, obj):
return ', '.join([author.get_full_name() for author in obj.authors.all()])
author_names.short_description = "Authors"
+
+ def make_issue(self, request, queryset):
+ opts = NewsletterIssue._meta
+ info = opts.app_label, opts.module_name
+ url = reverse("admin:%s_%s_add" % info)
+ return HttpResponseRedirect("%s?articles=%s" % (url, ",".join([str(a.pk) for a in queryset])))
+ make_issue.short_description = u"Create issue from selected %(verbose_name_plural)s"
class NewsletterIssueAdmin(TitledAdmin):
'classes': COLLAPSE_CLASSES
}),
('Feeds', {
- 'fields': ( 'feeds_enabled', 'feed_suffix', 'feed_type', 'item_title_template', 'item_description_template',),
+ 'fields': ( 'feeds_enabled', 'feed_suffix', 'feed_type', 'feed_length', 'item_title_template', 'item_description_template',),
'classes': COLLAPSE_CLASSES
})
)
--- /dev/null
+class HttpNotAcceptable(Exception):
+ """This will be raised if an Http-Accept header will not accept the feed content types that are available."""
+ pass
\ No newline at end of file
--- /dev/null
+from django.http import HttpResponse
+from django.utils.decorators import decorator_from_middleware
+from philo.contrib.penfield.exceptions import HttpNotAcceptable
+
+
+class HttpNotAcceptableMiddleware(object):
+ """Middleware to catch HttpNotAcceptable errors and return an Http406 response.
+ See RFC 2616."""
+ def process_exception(self, request, exception):
+ if isinstance(exception, HttpNotAcceptable):
+ return HttpResponse(status=406)
+
+
+http_not_acceptable = decorator_from_middleware(HttpNotAcceptableMiddleware)
\ No newline at end of file
def forwards(self, orm):
# Adding field 'NewsletterView.feed_type'
- db.add_column('penfield_newsletterview', 'feed_type', self.gf('django.db.models.fields.CharField')(default='atom', max_length=50), keep_default=False)
+ db.add_column('penfield_newsletterview', 'feed_type', self.gf('django.db.models.fields.CharField')(default='application/atom+xml', max_length=50), keep_default=False)
# Adding field 'NewsletterView.item_title_template'
db.add_column('penfield_newsletterview', 'item_title_template', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='penfield_newsletterview_title_related', null=True, to=orm['philo.Template']), keep_default=False)
--- /dev/null
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'NewsletterView.feed_length'
+ db.add_column('penfield_newsletterview', 'feed_length', self.gf('django.db.models.fields.PositiveIntegerField')(default=15, null=True, blank=True), keep_default=False)
+
+ # Adding field 'BlogView.feed_length'
+ db.add_column('penfield_blogview', 'feed_length', self.gf('django.db.models.fields.PositiveIntegerField')(default=15, null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'NewsletterView.feed_length'
+ db.delete_column('penfield_newsletterview', 'feed_length')
+
+ # Deleting field 'BlogView.feed_length'
+ db.delete_column('penfield_blogview', 'feed_length')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'oberlin.person': {
+ 'Meta': {'object_name': 'Person'},
+ 'bio': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '70', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'penfield.blog': {
+ 'Meta': {'object_name': 'Blog'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.blogentry': {
+ 'Meta': {'object_name': 'BlogEntry'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['oberlin.Person']"}),
+ 'blog': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'entries'", 'null': 'True', 'to': "orm['penfield.Blog']"}),
+ 'content': ('django.db.models.fields.TextField', [], {}),
+ 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
+ 'excerpt': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'blogentries'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['philo.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.blogview': {
+ 'Meta': {'object_name': 'BlogView'},
+ 'blog': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogviews'", 'to': "orm['penfield.Blog']"}),
+ 'entries_per_page': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'entry_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_entry_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'entry_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_entry_related'", 'to': "orm['philo.Page']"}),
+ 'entry_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'entries'", 'max_length': '255'}),
+ 'entry_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
+ 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}),
+ 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'application/atom+xml; charset=utf8'", 'max_length': '50'}),
+ 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_index_related'", 'to': "orm['philo.Page']"}),
+ 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'tag_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_tag_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'tag_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_tag_related'", 'to': "orm['philo.Page']"}),
+ 'tag_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'tags'", 'max_length': '255'})
+ },
+ 'penfield.newsletter': {
+ 'Meta': {'object_name': 'Newsletter'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterarticle': {
+ 'Meta': {'unique_together': "(('newsletter', 'slug'),)", 'object_name': 'NewsletterArticle'},
+ 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['oberlin.Person']"}),
+ 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
+ 'full_text': ('philo.models.fields.TemplateField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'lede': ('philo.models.fields.TemplateField', [], {'null': 'True', 'blank': 'True'}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'articles'", 'to': "orm['penfield.Newsletter']"}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'newsletterarticles'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['philo.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterissue': {
+ 'Meta': {'unique_together': "(('newsletter', 'numbering'),)", 'object_name': 'NewsletterIssue'},
+ 'articles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'issues'", 'symmetrical': 'False', 'to': "orm['penfield.NewsletterArticle']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issues'", 'to': "orm['penfield.Newsletter']"}),
+ 'numbering': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterview': {
+ 'Meta': {'object_name': 'NewsletterView'},
+ 'article_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_article_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'article_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_article_related'", 'to': "orm['philo.Page']"}),
+ 'article_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'articles'", 'max_length': '255'}),
+ 'article_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
+ 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}),
+ 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'application/atom+xml; charset=utf8'", 'max_length': '50'}),
+ 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_index_related'", 'to': "orm['philo.Page']"}),
+ 'issue_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_issue_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'issue_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_issue_related'", 'to': "orm['philo.Page']"}),
+ 'issue_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'issues'", 'max_length': '255'}),
+ 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletterviews'", 'to': "orm['penfield.Newsletter']"})
+ },
+ 'philo.attribute': {
+ 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+ 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.node': {
+ 'Meta': {'object_name': 'Node'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+ },
+ 'philo.template': {
+ 'Meta': {'object_name': 'Template'},
+ 'code': ('philo.models.fields.TemplateField', [], {}),
+ 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+ }
+ }
+
+ complete_apps = ['penfield']
from django.utils.encoding import smart_unicode, force_unicode
from django.utils.html import escape
from datetime import date, datetime
+from philo.contrib.penfield.exceptions import HttpNotAcceptable
+from philo.contrib.penfield.middleware import http_not_acceptable
from philo.contrib.penfield.validators import validate_pagination_count
from philo.exceptions import ViewCanNotProvideSubpath
from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField, Template
except:
mimeparse = None
+
ATOM = feedgenerator.Atom1Feed.mime_type
RSS = feedgenerator.Rss201rev2Feed.mime_type
FEEDS = SortedDict([
feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM)
feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
feeds_enabled = models.BooleanField(default=True)
+ feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.")
item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
description = ""
- def feed_patterns(self, get_items_attr, page_attr, reverse_name):
+ def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
"""
Given the name to be used to reverse this view and the names of
the attributes for the function that fetches the objects, returns
patterns suitable for inclusion in urlpatterns.
"""
- urlpatterns = patterns('',
- url(r'^$', self.page_view(get_items_attr, page_attr), name=reverse_name)
- )
+ urlpatterns = patterns('')
if self.feeds_enabled:
feed_reverse_name = "%s_feed" % reverse_name
+ feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name))
+ feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
urlpatterns += patterns('',
- url(r'^%s$' % self.feed_suffix, self.feed_view(get_items_attr, feed_reverse_name), name=feed_reverse_name),
+ url(feed_pattern, feed_view, name=feed_reverse_name),
)
+ urlpatterns += patterns('',
+ url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
+ )
return urlpatterns
def get_object(self, request, **kwargs):
def get_feed_type(self, request):
feed_type = self.feed_type
+ if feed_type not in FEEDS:
+ feed_type = FEEDS.keys()[0]
accept = request.META.get('HTTP_ACCEPT')
if accept and feed_type not in accept and "*/*" not in accept and "%s/*" % feed_type.split("/")[0] not in accept:
# Wups! They aren't accepting the chosen format. Is there another format we can use?
else:
feed_type = None
if not feed_type:
- # See RFC 2616
- return HttpResponse(status=406)
+ raise HttpNotAcceptable
return FEEDS[feed_type]
def get_feed(self, obj, request, reverse_name):
except Site.DoesNotExist:
current_site = RequestSite(request)
+ if self.feed_length is not None:
+ items = items[:self.feed_length]
+
for item in items:
if title_template is not None:
title = title_template.render(RequestContext(request, {'obj': item}))
index_page = models.ForeignKey(Page, related_name='blog_index_related')
entry_page = models.ForeignKey(Page, related_name='blog_entry_related')
+ # TODO: entry_archive is misleading. Rename to ymd_page or timespan_page.
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)
if self.entry_permalink_style == 'D':
kwargs.update({'day': str(obj.date.day).zfill(2)})
return self.entry_view, [], kwargs
- elif isinstance(obj, Tag) or (isinstance(obj, models.QuerySet) and obj.model == Tag and obj):
+ elif isinstance(obj, Tag) or (isinstance(obj, models.query.QuerySet) and obj.model == Tag and obj):
if isinstance(obj, Tag):
obj = [obj]
slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()]
@property
def urlpatterns(self):
- urlpatterns = patterns('',
- url(r'^', include(self.feed_patterns('get_all_entries', 'index_page', 'index'))),
- )
- if self.feeds_enabled:
- urlpatterns += patterns('',
- url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)/%s$' % (self.tag_permalink_base, self.feed_suffix), self.feed_view('get_entries_by_tag', 'entries_by_tag_feed'), name='entries_by_tag_feed'),
- )
- urlpatterns += patterns('',
- url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)$' % self.tag_permalink_base, self.page_view('get_entries_by_tag', 'tag_page'), name='entries_by_tag')
- )
+ urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') +\
+ self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)$' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'entries_by_tag')
+
if self.tag_archive_page:
urlpatterns += patterns('',
url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
if self.entry_archive_page:
if self.entry_permalink_style in 'DMY':
- urlpatterns += patterns('',
- url(r'^(?P<year>\d{4})', include(self.feed_patterns('get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')))
- )
+ urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')
if self.entry_permalink_style in 'DM':
- urlpatterns += patterns('',
- url(r'^(?P<year>\d{4})/(?P<month>\d{2})$', include(self.feed_patterns('get_entries_by_ymd', 'entry_archive_page', 'entries_by_month'))),
- )
+ urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_month')
if self.entry_permalink_style == 'D':
- urlpatterns += patterns('',
- url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})$', include(self.feed_patterns('get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')))
- )
+ urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')
if self.entry_permalink_style == 'D':
urlpatterns += patterns('',
if 'tags' in extra_context:
tags = extra_context['tags']
- feed.feed['link'] = request.node.construct_url(self.reverse(tags), with_domain=True, request=request, secure=request.is_secure())
+ feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure())
else:
tags = obj.entry_tags
@property
def urlpatterns(self):
- urlpatterns = patterns('',
- url(r'^', include(self.feed_patterns('get_all_articles', 'index_page', 'index'))),
- url(r'^%s/(?P<numbering>.+)' % self.issue_permalink_base, include(self.feed_patterns('get_articles_by_issue', 'issue_page', 'issue')))
+ urlpatterns = self.feed_patterns(r'^', 'get_all_articles', 'index_page', 'index') + patterns('',
+ url(r'^%s/(?P<numbering>.+)$' % self.issue_permalink_base, self.page_view('get_articles_by_issue', 'issue_page'), name='issue')
)
if self.issue_archive_page:
urlpatterns += patterns('',
url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
)
if self.article_permalink_style in 'DMY':
- urlpatterns += patterns('',
- url(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, include(self.feed_patterns('get_articles_by_ymd', 'article_archive_page', 'articles_by_year')))
- )
+ urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
if self.article_permalink_style in 'DM':
- urlpatterns += patterns('',
- url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})' % self.article_permalink_base, include(self.feed_patterns('get_articles_by_ymd', 'article_archive_page', 'articles_by_month')))
- )
+ urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_month')
if self.article_permalink_style == 'D':
- urlpatterns += patterns('',
- url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})' % self.article_permalink_base, include(self.feed_patterns('get_articles_by_ymd', 'article_archive_page', 'articles_by_day')))
- )
+ urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_day')
if self.article_permalink_style == 'Y':
urlpatterns += patterns('',
# about that. TODO: Benchmark it.
caches = self.__class__._cache[self.db][node].values()
- items = []
+ target_pks = set()
for cache in caches:
- items += cache['items']
+ target_pks |= set([item.target_node_id for item in cache['items']])
# A distinct query is not strictly necessary. TODO: benchmark the efficiency
# with/without distinct.
- targets = list(Node.objects.filter(shipherd_navigationitem_related__in=items).distinct())
+ targets = list(Node.objects.filter(pk__in=target_pks).distinct())
for cache in caches:
for item in cache['items']:
- item.target_node = targets[targets.index(item.target_node)]
+ if item.target_node_id:
+ item.target_node = targets[targets.index(item.target_node)]
def clear_cache(self):
self.__class__._cache.pop(self.db, None)
from django.utils.safestring import mark_safe
from philo.contrib.shipherd.models import Navigation
from philo.models import Node
-from mptt.templatetags.mptt_tags import RecurseTreeNode, cache_tree_children
+from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
register = template.Library()
-class RecurseNavigationNode(RecurseTreeNode):
- def __init__(self, template_nodes, instance_var, key):
+class LazyNavigationRecurser(object):
+ def __init__(self, template_nodes, items, context, request):
self.template_nodes = template_nodes
- self.instance_var = instance_var
- self.key = key
+ self.items = items
+ self.context = context
+ self.request = request
- def _render_node(self, context, item, request):
- bits = []
+ def __call__(self):
+ items = self.items
+ context = self.context
+ request = self.request
+
+ if not items:
+ return ''
+
+ if 'navloop' in context:
+ parentloop = context['navloop']
+ else:
+ parentloop = {}
context.push()
- for child in item.get_children():
- context['item'] = child
- bits.append(self._render_node(context, child, request))
- context['item'] = item
- context['children'] = mark_safe(u''.join(bits))
- context['active'] = item.is_active(request)
- context['active_descendants'] = item.has_active_descendants(request)
- rendered = self.template_nodes.render(context)
+
+ depth = items[0].get_level()
+ len_items = len(items)
+
+ loop_dict = context['navloop'] = {
+ 'parentloop': parentloop,
+ 'depth': depth + 1,
+ 'depth0': depth
+ }
+
+ bits = []
+
+ for i, item in enumerate(items):
+ # First set context variables.
+ loop_dict['counter0'] = i
+ loop_dict['counter'] = i + 1
+ loop_dict['revcounter'] = len_items - i
+ loop_dict['revcounter0'] = len_items - i - 1
+ loop_dict['first'] = (i == 0)
+ loop_dict['last'] = (i == len_items - 1)
+
+ # Set on loop_dict and context for backwards-compatibility.
+ # Eventually only allow access through the loop_dict.
+ loop_dict['active'] = context['active'] = item.is_active(request)
+ loop_dict['active_descendants'] = context['active_descendants'] = item.has_active_descendants(request)
+
+ # Set these directly in the context for easy access.
+ context['item'] = item
+ context['children'] = self.__class__(self.template_nodes, item.get_children(), context, request)
+
+ # Then render the nodelist bit by bit.
+ for node in self.template_nodes:
+ bits.append(node.render(context))
context.pop()
- return rendered
+ return mark_safe(''.join(bits))
+
+
+class RecurseNavigationNode(template.Node):
+ def __init__(self, template_nodes, instance_var, key):
+ self.template_nodes = template_nodes
+ self.instance_var = instance_var
+ self.key = key
def render(self, context):
try:
instance = self.instance_var.resolve(context)
try:
- navigation = instance.navigation[self.key]
+ items = instance.navigation[self.key]
except:
return settings.TEMPLATE_STRING_IF_INVALID
- bits = [self._render_node(context, item, request) for item in navigation]
- return ''.join(bits)
+ return LazyNavigationRecurser(self.template_nodes, items, context, request)()
@register.tag
def recursenavigation(parser, token):
"""
- Based on django-mptt's recursetree templatetag. In addition to {{ item }} and {{ children }},
- sets {{ active }} and {{ active_descendants }} in the context.
+ The recursenavigation templatetag takes two arguments:
+ - the node for which the navigation should be found
+ - the navigation's key.
+
+ It will then recursively loop over each item in the navigation and render the template
+ chunk within the block. recursenavigation sets the following variables in the context:
- Note that the tag takes one variable, which is a Node instance.
+ ============================== ================================================
+ Variable Description
+ ============================== ================================================
+ ``navloop.depth`` The current depth of the loop (1 is the top level)
+ ``navloop.depth0`` The current depth of the loop (0 is the top level)
+ ``navloop.counter`` The current iteration of the current level(1-indexed)
+ ``navloop.counter0`` The current iteration of the current level(0-indexed)
+ ``navloop.first`` True if this is the first time through the current level
+ ``navloop.last`` True if this is the last time through the current level
+ ``navloop.parentloop`` This is the loop one level "above" the current one
+ ============================== ================================================
+ ``item`` The current item in the loop (a NavigationItem instance)
+ ``children`` If accessed, performs the next level of recursion.
+ ``navloop.active`` True if the item is active for this request
+ ``navloop.active_descendants`` True if the item has active descendants for this request
+ ============================== ================================================
- Usage:
+ Example:
<ul>
{% recursenavigation node main %}
- <li{% if active %} class='active'{% endif %}>
- {{ navigation.text }}
- {% if navigation.get_children %}
+ <li{% if navloop.active %} class='active'{% endif %}>
+ {{ navloop.item.text }}
+ {% if item.get_children %}
<ul>
{{ children }}
</ul>
instance_var = parser.compile_filter(bits[1])
key = bits[2]
- template_nodes = parser.parse(('endrecursenavigation',))
- parser.delete_first_token()
+ template_nodes = parser.parse(('recurse', 'endrecursenavigation',))
+
+ token = parser.next_token()
+ if token.contents == 'recurse':
+ template_nodes.append(RecurseNavigationMarker())
+ template_nodes.extend(parser.parse(('endrecursenavigation')))
+ parser.delete_first_token()
return RecurseNavigationNode(template_nodes, instance_var, key)
@register.filter
def has_navigation(node, key=None):
- nav = node.navigation
- if key is not None:
- if key in nav and bool(node.navigation[key]):
- return True
- elif key not in node.navigation:
- return False
- return bool(node.navigation)
+ try:
+ nav = node.navigation
+ if key is not None:
+ if key in nav and bool(node.navigation[key]):
+ return True
+ elif key not in node.navigation:
+ return False
+ return bool(node.navigation)
+ except:
+ return False
@register.filter
from datetime import date
from django import forms
from django.conf import settings
+from django.contrib.auth import authenticate
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from philo.contrib.waldo.tokens import REGISTRATION_TIMEOUT_DAYS
-LOGIN_FORM_KEY = 'this_is_the_login_form'
-LoginForm = type('LoginForm', (AuthenticationForm,), {
- LOGIN_FORM_KEY: forms.BooleanField(widget=forms.HiddenInput, initial=True)
-})
-
-
class EmailInput(forms.TextInput):
input_type = 'email'
class Meta:
model = User
- fields = ('first_name', 'last_name', 'email')
\ No newline at end of file
+ fields = ('first_name', 'last_name', 'email')
+
+
+class WaldoAuthenticationForm(AuthenticationForm):
+ ERROR_MESSAGE = _("Please enter a correct username and password. Note that both fields are case-sensitive.")
+
+ def clean(self):
+ username = self.cleaned_data.get('username')
+ password = self.cleaned_data.get('password')
+ message = self.ERROR_MESSAGE
+
+ if username and password:
+ self.user_cache = authenticate(username=username, password=password)
+ if self.user_cache is None:
+ if u'@' in username:
+ # Maybe they entered their email? Look it up, but still raise a ValidationError.
+ try:
+ user = User.objects.get(email=username)
+ except (User.DoesNotExist, User.MultipleObjectsReturned):
+ pass
+ else:
+ if user.check_password(password):
+ message = _("Your e-mail address is not your username. Try '%s' instead.") % user.username
+ raise ValidationError(message)
+ elif not self.user_cache.is_active:
+ raise ValidationError(message)
+ self.check_for_test_cookie()
+ return self.cleaned_data
+
+ def check_for_test_cookie(self):
+ # This method duplicates the Django 1.3 AuthenticationForm method.
+ if self.request and not self.request.session.test_cookie_worked():
+ raise forms.ValidationError(
+ _("Your Web browser doesn't appear to have cookies enabled. "
+ "Cookies are required for logging in."))
\ No newline at end of file
from django.shortcuts import render_to_response, get_object_or_404
from django.template.defaultfilters import striptags
from django.utils.http import int_to_base36, base36_to_int
-from django.utils.translation import ugettext_lazy, ugettext as _
+from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from philo.models import MultiView, Page
-from philo.contrib.waldo.forms import LOGIN_FORM_KEY, LoginForm, RegistrationForm, UserAccountForm
+from philo.contrib.waldo.forms import WaldoAuthenticationForm, RegistrationForm, UserAccountForm
from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
import urlparse
-ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
-
-
class LoginMultiView(MultiView):
"""
- Handles login, registration, and forgotten passwords. In other words, this
- multiview provides exclusively view and methods related to usernames and
- passwords.
+ Handles exclusively methods and views related to logging users in and out.
"""
login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
- password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related')
- password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related')
- password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related')
- password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
- register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related')
- register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related')
+ login_form = WaldoAuthenticationForm
@property
def urlpatterns(self):
- urlpatterns = patterns('',
+ return patterns('',
url(r'^login$', self.login, name='login'),
url(r'^logout$', self.logout, name='logout'),
-
- url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
- url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
-
- url(r'^register$', csrf_protect(self.register), name='register'),
- url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
)
-
- if self.password_change_page:
- urlpatterns += patterns('',
- url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
- )
-
- return urlpatterns
- def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
- token = token_generator.make_token(user, *(token_args or []))
- kwargs = {
- 'uidb36': int_to_base36(user.id),
- 'token': token
- }
- kwargs.update(reverse_kwargs or {})
- return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True)
-
- def display_login_page(self, request, message, extra_context=None):
- request.session.set_test_cookie()
-
- referrer = request.META.get('HTTP_REFERER', None)
-
- if referrer is not None:
- referrer = urlparse.urlparse(referrer)
- host = referrer[1]
- if host != request.get_host():
- referrer = None
- else:
- redirect = '%s?%s' % (referrer[2], referrer[4])
+ def set_requirement_redirect(self, request, redirect=None):
+ "Figure out where someone should end up after landing on a `requirement` page like the login page."
+ if redirect is not None:
+ pass
+ elif 'requirement_redirect' in request.session:
+ return
+ else:
+ referrer = request.META.get('HTTP_REFERER', None)
- if referrer is None:
- redirect = request.node.get_absolute_url()
+ if referrer is not None:
+ referrer = urlparse.urlparse(referrer)
+ host = referrer[1]
+ if host != request.get_host():
+ referrer = None
+ else:
+ redirect = '%s?%s' % (referrer[2], referrer[4])
- path = request.get_full_path()
- if redirect != path:
- if redirect is None:
- redirect = '/'.join(path.split('/')[:-2])
- request.session['redirect'] = redirect
+ path = request.get_full_path()
+ if referrer is None or redirect == path:
+ # Default to the index page if we can't find a referrer or
+ # if we'd otherwise redirect to where we already are.
+ redirect = request.node.get_absolute_url()
- if request.POST:
- form = LoginForm(request.POST)
+ request.session['requirement_redirect'] = redirect
+
+ def get_requirement_redirect(self, request, default=None):
+ redirect = request.session.pop('requirement_redirect', None)
+ # Security checks a la django.contrib.auth.views.login
+ if not redirect or ' ' in redirect:
+ redirect = default
else:
- form = LoginForm()
- context = self.get_context()
- context.update(extra_context or {})
- context.update({
- 'message': message,
- 'form': form
- })
- return self.login_page.render_to_response(request, extra_context=context)
+ netloc = urlparse.urlparse(redirect)[1]
+ if netloc and netloc != request.get_host():
+ redirect = default
+ if redirect is None:
+ redirect = request.node.get_absolute_url()
+ return redirect
+ @never_cache
def login(self, request, extra_context=None):
"""
Displays the login form for the given HttpRequest.
"""
- if request.user.is_authenticated():
- return HttpResponseRedirect(request.node.get_absolute_url())
+ self.set_requirement_redirect(request)
- context = self.get_context()
- context.update(extra_context or {})
-
- from django.contrib.auth.models import User
+ # Redirect already-authenticated users to the index page.
+ if request.user.is_authenticated():
+ messages.add_message(request, messages.INFO, "You are already authenticated. Please log out if you wish to log in as a different user.")
+ return HttpResponseRedirect(self.get_requirement_redirect(request))
- # If this isn't already the login page, display it.
- if not request.POST.has_key(LOGIN_FORM_KEY):
- if request.POST:
- message = _("Please log in again, because your session has expired.")
- else:
- message = ""
- return self.display_login_page(request, message, context)
-
- # Check that the user accepts cookies.
- if not request.session.test_cookie_worked():
- message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
- return self.display_login_page(request, message, context)
+ if request.method == 'POST':
+ form = self.login_form(request=request, data=request.POST)
+ if form.is_valid():
+ redirect = self.get_requirement_redirect(request)
+ login(request, form.get_user())
+
+ if request.session.test_cookie_worked():
+ request.session.delete_test_cookie()
+
+ return HttpResponseRedirect(redirect)
else:
- request.session.delete_test_cookie()
+ form = self.login_form()
- # Check the password.
- username = request.POST.get('username', None)
- password = request.POST.get('password', None)
- user = authenticate(username=username, password=password)
- if user is None:
- message = ERROR_MESSAGE
- if username is not None and u'@' in username:
- # Mistakenly entered e-mail address instead of username? Look it up.
- try:
- user = User.objects.get(email=username)
- except (User.DoesNotExist, User.MultipleObjectsReturned):
- message = _("Usernames cannot contain the '@' character.")
- else:
- if user.check_password(password):
- message = _("Your e-mail address is not your username."
- " Try '%s' instead.") % user.username
- else:
- message = _("Usernames cannot contain the '@' character.")
- return self.display_login_page(request, message, context)
-
- # The user data is correct; log in the user in and continue.
- else:
- if user.is_active:
- login(request, user)
- try:
- redirect = request.session.pop('redirect')
- except KeyError:
- redirect = request.node.get_absolute_url()
- return HttpResponseRedirect(redirect)
- else:
- return self.display_login_page(request, ERROR_MESSAGE, context)
- login = never_cache(login)
+ request.session.set_test_cookie()
+
+ context = self.get_context()
+ context.update(extra_context or {})
+ context.update({
+ 'form': form
+ })
+ return self.login_page.render_to_response(request, extra_context=context)
- def logout(self, request):
+ @never_cache
+ def logout(self, request, extra_context=None):
return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
def login_required(self, view):
def inner(request, *args, **kwargs):
if not request.user.is_authenticated():
+ self.set_requirement_redirect(request, redirect=request.path)
+ if request.POST:
+ messages.add_message(request, messages.ERROR, "Please log in again, because your session has expired.")
return HttpResponseRedirect(self.reverse('login', node=request.node))
return view(request, *args, **kwargs)
return inner
+ class Meta:
+ abstract = True
+
+
+class PasswordMultiView(LoginMultiView):
+ "Adds on views for password-related functions."
+ password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related', blank=True, null=True)
+ password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related', blank=True, null=True)
+ password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related', blank=True, null=True)
+ password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
+
+ password_change_form = PasswordChangeForm
+ password_set_form = SetPasswordForm
+ password_reset_form = PasswordResetForm
+
+ @property
+ def urlpatterns(self):
+ urlpatterns = super(PasswordMultiView, self).urlpatterns
+
+ if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
+ urlpatterns += patterns('',
+ url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
+ url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
+ )
+
+ if self.password_change_page:
+ urlpatterns += patterns('',
+ url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
+ )
+ return urlpatterns
+
+ def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None, secure=False):
+ token = token_generator.make_token(user, *(token_args or []))
+ kwargs = {
+ 'uidb36': int_to_base36(user.id),
+ 'token': token
+ }
+ kwargs.update(reverse_kwargs or {})
+ return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True, secure=secure)
+
def send_confirmation_email(self, subject, email, page, extra_context):
text_content = page.render_to_string(extra_context=extra_context)
from_email = 'noreply@%s' % Site.objects.get_current().domain
return HttpResponseRedirect(request.node.get_absolute_url())
if request.method == 'POST':
- form = PasswordResetForm(request.POST)
+ form = self.password_reset_form(request.POST)
if form.is_valid():
current_site = Site.objects.get_current()
for user in form.users_cache:
context = {
- 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
+ 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node, secure=request.is_secure()),
+ 'user': user,
+ 'site': current_site,
+ 'request': request,
+
+ # Deprecated... leave in for backwards-compatibility
'username': user.username
}
self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
messages.add_message(request, messages.SUCCESS, "An email has been sent to the address you provided with details on resetting your password.", fail_silently=True)
return HttpResponseRedirect('')
else:
- form = PasswordResetForm()
+ form = self.password_reset_form()
context = self.get_context()
context.update(extra_context or {})
if token_generator.check_token(user, token):
if request.method == 'POST':
- form = SetPasswordForm(user, request.POST)
+ form = self.password_set_form(user, request.POST)
if form.is_valid():
form.save()
messages.add_message(request, messages.SUCCESS, "Password reset successful.")
return HttpResponseRedirect(self.reverse('login', node=request.node))
else:
- form = SetPasswordForm(user)
+ form = self.password_set_form(user)
context = self.get_context()
context.update(extra_context or {})
def password_change(self, request, extra_context=None):
if request.method == 'POST':
- form = PasswordChangeForm(request.user, request.POST)
+ form = self.password_change_form(request.user, request.POST)
if form.is_valid():
form.save()
messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
return HttpResponseRedirect('')
else:
- form = PasswordChangeForm(request.user)
+ form = self.password_change_form(request.user)
context = self.get_context()
context.update(extra_context or {})
})
return self.password_change_page.render_to_response(request, extra_context=context)
+ class Meta:
+ abstract = True
+
+
+class RegistrationMultiView(PasswordMultiView):
+ """Adds on the pages necessary for letting new users register."""
+ register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related', blank=True, null=True)
+ register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related', blank=True, null=True)
+ registration_form = RegistrationForm
+
+ @property
+ def urlpatterns(self):
+ urlpatterns = super(RegistrationMultiView, self).urlpatterns
+ if self.register_page and self.register_confirmation_email:
+ urlpatterns += patterns('',
+ url(r'^register$', csrf_protect(self.register), name='register'),
+ url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
+ )
+ return urlpatterns
+
def register(self, request, extra_context=None, token_generator=registration_token_generator):
if request.user.is_authenticated():
return HttpResponseRedirect(request.node.get_absolute_url())
if request.method == 'POST':
- form = RegistrationForm(request.POST)
+ form = self.registration_form(request.POST)
if form.is_valid():
user = form.save()
+ current_site = Site.objects.get_current()
context = {
- 'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
+ 'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node, secure=request.is_secure()),
+ 'user': user,
+ 'site': current_site,
+ 'request': request
}
- current_site = Site.objects.get_current()
self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
return HttpResponseRedirect(request.node.get_absolute_url())
else:
- form = RegistrationForm()
+ form = self.registration_form()
context = self.get_context()
context.update(extra_context or {})
authenticated_user = authenticate(username=user.username, password=temp_password)
login(request, authenticated_user)
finally:
- # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
+ # if anything goes wrong, do our best make sure that the true password is restored.
user.password = true_password
user.save()
return self.post_register_confirm_redirect(request)
abstract = True
-class AccountMultiView(LoginMultiView):
+class AccountMultiView(RegistrationMultiView):
"""
By default, the `account` consists of the first_name, last_name, and email fields
of the User model. Using a different account model is as simple as writing a form that
accepts a User instance as the first argument.
"""
- manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
- email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
+ manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
+ email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related', blank=True, null=True, help_text="If this is left blank, email changes will be performed without confirmation.")
+
account_form = UserAccountForm
@property
def urlpatterns(self):
urlpatterns = super(AccountMultiView, self).urlpatterns
- urlpatterns += patterns('',
- url(r'^account$', self.login_required(self.account_view), name='account'),
- url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
- )
+ if self.manage_account_page:
+ urlpatterns += patterns('',
+ url(r'^account$', self.login_required(self.account_view), name='account'),
+ )
+ if self.email_change_confirmation_email:
+ urlpatterns += patterns('',
+ url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
+ )
return urlpatterns
def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
form = self.account_form(request.user, request.POST, request.FILES)
if form.is_valid():
- if 'email' in form.changed_data:
- # ModelForms modify their instances in-place during validation,
- # so reset the instance's email to its previous value here,
- # then remove the new value from cleaned_data.
+ message = "Account information saved."
+ redirect = self.get_requirement_redirect(request, default='')
+ if 'email' in form.changed_data and self.email_change_confirmation_email:
+ # ModelForms modify their instances in-place during
+ # validation, so reset the instance's email to its
+ # previous value here, then remove the new value
+ # from cleaned_data. We only do this if an email
+ # change confirmation email is available.
request.user.email = form.initial['email']
email = form.cleaned_data.pop('email')
+ current_site = Site.objects.get_current()
+
context = {
- 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
+ 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')}, secure=request.is_secure()),
+ 'user': request.user,
+ 'site': current_site,
+ 'request': request
}
- current_site = Site.objects.get_current()
self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
- messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
+
+ message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "")
+ if not request.user.email:
+ message += " You will need to confirm the email before accessing pages that require a valid account."
+ redirect = ''
form.save()
- messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
- return HttpResponseRedirect('')
+
+ if redirect != '':
+ message += " Here you go!"
+
+ messages.add_message(request, messages.SUCCESS, message, fail_silently=True)
+ return HttpResponseRedirect(redirect)
else:
form = self.account_form(request.user)
def account_required(self, view):
def inner(request, *args, **kwargs):
if not self.has_valid_account(request.user):
- if not request.method == "POST":
- messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
- return self.account_view(request, *args, **kwargs)
+ messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
+ if self.manage_account_page:
+ self.set_requirement_redirect(request, redirect=request.path)
+ redirect = self.reverse('account', node=request.node)
+ else:
+ redirect = node.get_absolute_url()
+ return HttpResponseRedirect(redirect)
return view(request, *args, **kwargs)
inner = self.login_required(inner)
return inner
def post_register_confirm_redirect(self, request):
- messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
- return HttpResponseRedirect(self.reverse('account', node=request.node))
+ if self.manage_account_page:
+ messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
+ return HttpResponseRedirect(self.reverse('account', node=request.node))
+ return super(AccountMultiView, self).post_register_confirm_redirect(request)
def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
"""
user.email = email
user.save()
messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
- return HttpReponseRedirect(self.reverse('account', node=request.node))
+ if self.manage_account_page:
+ redirect = self.reverse('account', node=request.node)
+ else:
+ redirect = request.node.get_absolute_url()
+ return HttpResponseRedirect(redirect)
raise Http404
var tagCreation = window.tagCreation;
(function($) {
+ location_re = new RegExp("^https?:\/\/" + window.location.host + "/")
+
+ $('html').ajaxSend(function(event, xhr, settings) {
+ function getCookie(name) {
+ var cookieValue = null;
+ if (document.cookie && document.cookie != '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = $.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) == (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ }
+ if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url)) || location_re.test(settings.url)) {
+ // Only send the token to relative URLs i.e. locally.
+ xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
+ }
+ });
tagCreation = {
'cache': {},
'addTagFromSlug': function(triggeringLink) {
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import RegexValidator
from django.utils import simplejson as json
-from django.utils.encoding import smart_str
+from django.utils.encoding import force_unicode
from philo.exceptions import AncestorDoesNotExist
from philo.models.fields import JSONField
from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
class AttributeValue(models.Model):
attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
- @property
- def attribute(self):
- return self.attribute_set.all()[0]
-
def set_value(self, value):
raise NotImplementedError
value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null', db_index=True)
def __unicode__(self):
- return smart_str(self.value)
+ return force_unicode(self.value)
def value_formfields(self):
kwargs = {'initial': self.value_json}
from django.core.servers.basehttp import FileWrapper
from django.core.urlresolvers import resolve, clear_url_caches, reverse, NoReverseMatch
from django.template import add_to_builtins as register_templatetags
+from django.utils.encoding import smart_str
from inspect import getargspec
from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
try:
subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {})
- except NoReverseMatch:
- raise ViewCanNotProvideSubpath
+ except NoReverseMatch, e:
+ raise ViewCanNotProvideSubpath(e.message)
if node is not None:
return node.construct_url(subpath)
reversing_parameters = JSONField(blank=True, help_text="If reversing parameters are defined, url_or_subpath will instead be interpreted as the view name to be reversed.")
def clean(self):
- # Should this be enforced? Not enforcing it would allow creation of "headers" in the navbar.
if not self.target_node and not self.url_or_subpath:
raise ValidationError("Either a target node or a url must be defined.")
try:
self.get_target_url()
- except NoReverseMatch, e:
+ except (NoReverseMatch, ViewCanNotProvideSubpath), e:
raise ValidationError(e.message)
super(TargetURLModel, self).clean()
def get_reverse_params(self):
params = self.reversing_parameters
- args = isinstance(params, list) and params or None
- kwargs = isinstance(params, dict) and params or None
+ args = kwargs = None
+ if isinstance(params, list):
+ args = params
+ elif isinstance(params, dict):
+ # Convert unicode keys to strings for Python < 2.6.5. Compare
+ # http://stackoverflow.com/questions/4598604/how-to-pass-unicode-keywords-to-kwargs
+ kwargs = dict([(smart_str(k, 'ascii'), v) for k, v in params.items()])
return self.url_or_subpath, args, kwargs
def get_target_url(self):
abstract = True
-class Redirect(View, TargetURLModel):
+class Redirect(TargetURLModel, View):
STATUS_CODES = (
(302, 'Temporary'),
(301, 'Permanent'),
from django.core.exceptions import ValidationError
from django.db import models
from django.http import HttpResponse
-from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, add_to_builtins as register_templatetags
+from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, add_to_builtins as register_templatetags, TextNode, VariableNode
+from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext
+from django.utils.datastructures import SortedDict
from philo.models.base import TreeModel, register_value_model
from philo.models.fields import TemplateField
from philo.models.nodes import View
from philo.templatetags.containers import ContainerNode
-from philo.utils import fattr, nodelist_crawl
+from philo.utils import fattr
from philo.validators import LOADED_TEMPLATE_ATTR
from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
+class LazyContainerFinder(object):
+ def __init__(self, nodes):
+ self.nodes = nodes
+ self.initialized = False
+ self.contentlet_specs = set()
+ self.contentreference_specs = SortedDict()
+ self.blocks = {}
+ self.block_super = False
+
+ def process(self, nodelist):
+ for node in nodelist:
+ if isinstance(node, ContainerNode):
+ if not node.references:
+ self.contentlet_specs.add(node.name)
+ else:
+ if node.name not in self.contentreference_specs.keys():
+ self.contentreference_specs[node.name] = node.references
+ continue
+
+ if isinstance(node, BlockNode):
+ self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
+ block.initialize()
+ self.blocks.update(block.blocks)
+ continue
+
+ if isinstance(node, ExtendsNode):
+ continue
+
+ if isinstance(node, VariableNode):
+ if node.filter_expression.var.lookups == (u'block', u'super'):
+ self.block_super = True
+
+ if hasattr(node, 'child_nodelists'):
+ for nodelist_name in node.child_nodelists:
+ if hasattr(node, nodelist_name):
+ nodelist = getattr(node, nodelist_name)
+ self.process(nodelist)
+
+ # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
+ # node as rendering an additional template. Philo monkeypatches the attribute onto
+ # the relevant default nodes and declares it on any native nodes.
+ if hasattr(node, LOADED_TEMPLATE_ATTR):
+ loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
+ if loaded_template:
+ nodelist = loaded_template.nodelist
+ self.process(nodelist)
+
+ def initialize(self):
+ if not self.initialized:
+ self.process(self.nodes)
+ self.initialized = True
+
+
class Template(TreeModel):
name = models.CharField(max_length=255)
documentation = models.TextField(null=True, blank=True)
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 process_node(node, nodes):
- if isinstance(node, ContainerNode):
- nodes.append(node)
+ template = DjangoTemplate(self.code)
+
+ def build_extension_tree(nodelist):
+ nodelists = []
+ extends = None
+ for node in nodelist:
+ if not isinstance(node, TextNode):
+ if isinstance(node, ExtendsNode):
+ extends = node
+ break
+
+ if extends:
+ if extends.nodelist:
+ nodelists.append(LazyContainerFinder(extends.nodelist))
+ loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
+ nodelists.extend(build_extension_tree(loaded_template.nodelist))
+ else:
+ # Base case: root.
+ nodelists.append(LazyContainerFinder(nodelist))
+ return nodelists
+
+ # Build a tree of the templates we're using, placing the root template first.
+ levels = build_extension_tree(template.nodelist)[::-1]
+
+ contentlet_specs = set()
+ contentreference_specs = SortedDict()
+ blocks = {}
+
+ for level in levels:
+ level.initialize()
+ contentlet_specs |= level.contentlet_specs
+ contentreference_specs.update(level.contentreference_specs)
+ for name, block in level.blocks.items():
+ if block.block_super:
+ blocks.setdefault(name, []).append(block)
+ else:
+ blocks[name] = [block]
+
+ for block_list in blocks.values():
+ for block in block_list:
+ block.initialize()
+ contentlet_specs |= block.contentlet_specs
+ contentreference_specs.update(block.contentreference_specs)
- all_nodes = nodelist_crawl(DjangoTemplate(self.code).nodelist, process_node)
- 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
+ return contentlet_specs, contentreference_specs
def __unicode__(self):
return self.get_path(pathsep=u' › ', field='name')
{% comment %}Don't render the formset at all if there aren't any forms.{% endcomment %}
{% if inline_admin_formset.formset.forms %}
<fieldset class="module{% if inline_admin_formset.opts.classes %} {{ inline_admin_formset.opts.classes|join:" " }}{% endif %}">
- <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
+ <h2{% if "collapse" in inline_admin_formset.opts.classes %} class="collapse-handler"{% endif %}>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.non_form_errors }}
{% for inline_admin_form in inline_admin_formset %}
{% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}{% endspaceless %}
- <div class="row cells-{{ inline_admin_form.fields|length }}{% if not inline_admin_form.fields|length_is:"2" %} cells{% endif %}{% if inline_admin_form.errors %} errors{% endif %} {% for field in inline_admin_form %}{{ field.field.name }} {% endfor %}{% if forloop.last %} empty-form{% endif %}">
- <div{% if not inline_admin_form.fields|length_is:"2" %} class="cell"{% endif %}>
- <div class="column span-4"><label class='required' for="{{ inline_admin_form.form.content.auto_id }}{{ inline_admin_form.form.content_id.auto_id }}">{{ inline_admin_form.form.verbose_name|capfirst }}:</label>{{ inline_admin_form.form.name.as_hidden }}</div>
- {% for fieldset in inline_admin_form %}{% for line in fieldset %}{% for field in line %}
- {% if field.field.name != 'name' %}
+ {% endfor %}
+ {% for form in inline_admin_formset.formset.forms %}
+ <div class="row cells-{{ form.fields.keys|length }}{% if not form.fields.keys|length_is:"2" %} cells{% endif %}{% if form.errors %} errors{% endif %} {% for field in form %}{{ field.field.name }} {% endfor %}{% comment %} {% if forloop.last %} empty-form{% endif %}{% endcomment %}">
+ {{ form.non_field_errors }}
+ <div{% if not form.fields.keys|length_is:"2" %} class="cell"{% endif %}>
+ <div class="column span-4"><label class='required' for="{{ form.content.auto_id }}{{ form.content_id.auto_id }}">{{ form.verbose_name|capfirst }}:</label></div>
+ {% for field in form %}
+ {% if not field.is_hidden %}
<div class="column span-flexible">
- {% if field.is_readonly %}
- <p class="readonly">{{ field.contents }}</p>
- {% else %}
- {{ field.field }}
- {% endif %}
- {{ inline_admin_form.errors }}
- {% if field.field.field.help_text %}
- <p class="help">{{ field.field.field.help_text|safe }}</p>
+ {{ field }}
+ {{ field.errors }}
+ {% if field.field.help_text %}
+ <p class="help">{{ field.field.help_text|safe }}</p>
{% endif %}
</div>
{% endif %}
- {% endfor %}{% endfor %}{% endfor %}
+ {% endfor %}
</div>
</div>
{% endfor %}
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.non_form_errors }}
<table>
- <thead><tr>
- {% for field in inline_admin_formset.fields %}
- {% if not field.widget.is_hidden %}
- <th{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}</th>
- {% endif %}
- {% endfor %}
- {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
- </tr></thead>
-
<tbody>
{% for inline_admin_form in inline_admin_formset %}
{% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
- {{ inline_admin_form.form.name.as_hidden }}
{% endspaceless %}
- {% if inline_admin_form.form.non_field_errors %}
- <tr><td colspan="{{ inline_admin_form.field_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
+ {% endfor %}
+ {% for form in inline_admin_formset.formset.forms %}
+ {% if form.non_field_errors %}
+ <tr><td colspan="2">{{ form.non_field_errors }}</td></tr>
{% endif %}
- <tr class="{% cycle "row1" "row2" %} {% if forloop.last %} empty-form{% endif %}"
- id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
- <th>{{ inline_admin_form.form.verbose_name|capfirst }}:</th>
- {% for fieldset in inline_admin_form %}
- {% for line in fieldset %}
- {% for field in line %}
- {% if field.field.name != 'name' %}
+ <tr class="{% cycle "row1" "row2" %}{% comment %} {% if forloop.last %} empty-form{% endif %}{% endcomment %}"
+ id="{{ formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
+ <th>{{ form.verbose_name|capfirst }}:</th>
+ {% for field in form %}
+ {% if not field.is_hidden %}
<td class="{{ field.field.name }}">
- {% if field.is_readonly %}
- <p>{{ field.contents }}</p>
- {% else %}
{{ field.field.errors.as_ul }}
- {{ field.field }}
- {% endif %}
+ {{ field }}
+ {% if field.field.help_text %}
+ <p class="help">{{ field.field.help_text|safe }}</p>
+ {% endif %}
</td>
{% endif %}
{% endfor %}
- {% endfor %}
- {% endfor %}
- {% if inline_admin_formset.formset.can_delete %}
- <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
- {% endif %}
</tr>
{% endfor %}
</tbody>
# We ignore the IncludeNode because it will never work in a blank context.
setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended))
-setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
-
-
-def nodelist_crawl(nodelist, callback):
- """This function crawls through a template's nodelist and the nodelists of any included or extended
- templates, as determined by the presence and value of <LOADED_TEMPLATE_ATTR> on a node. Each node
- will also be passed to a callback function for additional processing."""
- results = []
- for node in nodelist:
- try:
- if hasattr(node, 'child_nodelists'):
- for nodelist_name in node.child_nodelists:
- if hasattr(node, nodelist_name):
- results.extend(nodelist_crawl(getattr(node, nodelist_name), callback))
-
- # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
- # node as rendering an additional template. Philo monkeypatches the attribute onto
- # the relevant default nodes and declares it on any native nodes.
- if hasattr(node, LOADED_TEMPLATE_ATTR):
- loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
- if loaded_template:
- results.extend(nodelist_crawl(loaded_template.nodelist, callback))
-
- callback(node, results)
- except:
- raise # fail for this node
- return results
\ No newline at end of file
+setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
\ No newline at end of file