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
}),
('Feed Settings', {
- '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
})
)
'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
--- /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
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")
urlpatterns = patterns('')
if self.feeds_enabled:
feed_reverse_name = "%s_feed" % reverse_name
- feed_view = self.feed_view(get_items_attr, 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(feed_pattern, feed_view, name=feed_reverse_name),
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}))
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.
- Note that the tag takes one variable, which is a Node instance.
+ 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:
- Usage:
+ ============================== ================================================
+ 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
+ ============================== ================================================
+
+ 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)
--- /dev/null
+from philo.contrib.sobol.search import *
\ No newline at end of file
--- /dev/null
+from django.conf import settings
+from django.conf.urls.defaults import patterns, url
+from django.contrib import admin
+from django.core.urlresolvers import reverse
+from django.db.models import Count
+from django.http import HttpResponseRedirect, Http404
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.utils.functional import update_wrapper
+from django.utils.translation import ugettext_lazy as _
+from philo.admin import EntityAdmin
+from philo.contrib.sobol.models import Search, ResultURL, SearchView
+
+
+class ResultURLInline(admin.TabularInline):
+ model = ResultURL
+ readonly_fields = ('url',)
+ can_delete = False
+ extra = 0
+ max_num = 0
+
+
+class SearchAdmin(admin.ModelAdmin):
+ readonly_fields = ('string',)
+ inlines = [ResultURLInline]
+ list_display = ['string', 'unique_urls', 'total_clicks']
+ search_fields = ['string', 'result_urls__url']
+ actions = ['results_action']
+ if 'grappelli' in settings.INSTALLED_APPS:
+ results_template = 'admin/sobol/search/grappelli_results.html'
+ else:
+ results_template = 'admin/sobol/search/results.html'
+
+ def get_urls(self):
+ urlpatterns = super(SearchAdmin, self).get_urls()
+
+ def wrap(view):
+ def wrapper(*args, **kwargs):
+ return self.admin_site.admin_view(view)(*args, **kwargs)
+ return update_wrapper(wrapper, view)
+
+ info = self.model._meta.app_label, self.model._meta.module_name
+
+ urlpatterns = patterns('',
+ url(r'^results/$', wrap(self.results_view), name="%s_%s_selected_results" % info),
+ url(r'^(.+)/results/$', wrap(self.results_view), name="%s_%s_results" % info)
+ ) + urlpatterns
+ return urlpatterns
+
+ def unique_urls(self, obj):
+ return obj.unique_urls
+ unique_urls.admin_order_field = 'unique_urls'
+
+ def total_clicks(self, obj):
+ return obj.total_clicks
+ total_clicks.admin_order_field = 'total_clicks'
+
+ def queryset(self, request):
+ qs = super(SearchAdmin, self).queryset(request)
+ return qs.annotate(total_clicks=Count('result_urls__clicks', distinct=True), unique_urls=Count('result_urls', distinct=True))
+
+ def results_action(self, request, queryset):
+ info = self.model._meta.app_label, self.model._meta.module_name
+ if len(queryset) == 1:
+ return HttpResponseRedirect(reverse("admin:%s_%s_results" % info, args=(queryset[0].pk,)))
+ else:
+ url = reverse("admin:%s_%s_selected_results" % info)
+ return HttpResponseRedirect("%s?ids=%s" % (url, ','.join([str(item.pk) for item in queryset])))
+ results_action.short_description = "View results for selected %(verbose_name_plural)s"
+
+ def results_view(self, request, object_id=None, extra_context=None):
+ if object_id is not None:
+ object_ids = [object_id]
+ else:
+ object_ids = request.GET.get('ids').split(',')
+
+ if object_ids is None:
+ raise Http404
+
+ qs = self.queryset(request).filter(pk__in=object_ids)
+ opts = self.model._meta
+
+ if len(object_ids) == 1:
+ title = _(u"Search results for %s" % qs[0])
+ else:
+ title = _(u"Search results for multiple objects")
+
+ context = {
+ 'title': title,
+ 'queryset': qs,
+ 'opts': opts,
+ 'root_path': self.admin_site.root_path,
+ 'app_label': opts.app_label
+ }
+ return render_to_response(self.results_template, context, context_instance=RequestContext(request))
+
+
+class SearchViewAdmin(EntityAdmin):
+ raw_id_fields = ('results_page',)
+ related_lookup_fields = {'fk': raw_id_fields}
+
+
+admin.site.register(Search, SearchAdmin)
+admin.site.register(SearchView, SearchViewAdmin)
\ No newline at end of file
--- /dev/null
+from django import forms
+from philo.contrib.sobol.utils import SEARCH_ARG_GET_KEY
+
+
+class BaseSearchForm(forms.BaseForm):
+ base_fields = {
+ SEARCH_ARG_GET_KEY: forms.CharField()
+ }
+
+
+class SearchForm(forms.Form, BaseSearchForm):
+ pass
\ No newline at end of file
--- /dev/null
+from django.conf.urls.defaults import patterns, url
+from django.contrib import messages
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.http import HttpResponseRedirect, Http404, HttpResponse
+from django.utils import simplejson as json
+from django.utils.datastructures import SortedDict
+from philo.contrib.sobol import registry
+from philo.contrib.sobol.forms import SearchForm
+from philo.contrib.sobol.utils import HASH_REDIRECT_GET_KEY, URL_REDIRECT_GET_KEY, SEARCH_ARG_GET_KEY, check_redirect_hash
+from philo.exceptions import ViewCanNotProvideSubpath
+from philo.models import MultiView, Page
+from philo.models.fields import SlugMultipleChoiceField
+from philo.validators import RedirectValidator
+import datetime
+try:
+ import eventlet
+except:
+ eventlet = False
+
+
+class Search(models.Model):
+ string = models.TextField()
+
+ def __unicode__(self):
+ return self.string
+
+ def get_weighted_results(self, threshhold=None):
+ "Returns this search's results ordered by decreasing weight."
+ if not hasattr(self, '_weighted_results'):
+ result_qs = self.result_urls.all()
+
+ if threshhold is not None:
+ result_qs = result_qs.filter(counts__datetime__gte=threshhold)
+
+ results = [result for result in result_qs]
+
+ results.sort(cmp=lambda x,y: cmp(y.weight, x.weight))
+
+ self._weighted_results = results
+
+ return self._weighted_results
+
+ def get_favored_results(self, error=5, threshhold=None):
+ """
+ Calculate the set of most-favored results. A higher error
+ will cause this method to be more reticent about adding new
+ items.
+
+ The thought is to see whether there are any results which
+ vastly outstrip the other options. As such, evenly-weighted
+ results should be grouped together and either added or
+ excluded as a group.
+ """
+ if not hasattr(self, '_favored_results'):
+ results = self.get_weighted_results(threshhold)
+
+ grouped_results = SortedDict()
+
+ for result in results:
+ grouped_results.setdefault(result.weight, []).append(result)
+
+ self._favored_results = []
+
+ for value, subresults in grouped_results.items():
+ cost = error * sum([(value - result.weight)**2 for result in self._favored_results])
+ if value > cost:
+ self._favored_results += subresults
+ else:
+ break
+ return self._favored_results
+
+ class Meta:
+ ordering = ['string']
+ verbose_name_plural = 'searches'
+
+
+class ResultURL(models.Model):
+ search = models.ForeignKey(Search, related_name='result_urls')
+ url = models.TextField(validators=[RedirectValidator()])
+
+ def __unicode__(self):
+ return self.url
+
+ def get_weight(self, threshhold=None):
+ if not hasattr(self, '_weight'):
+ clicks = self.clicks.all()
+
+ if threshhold is not None:
+ clicks = clicks.filter(datetime__gte=threshhold)
+
+ self._weight = sum([click.weight for click in clicks])
+
+ return self._weight
+ weight = property(get_weight)
+
+ class Meta:
+ ordering = ['url']
+
+
+class Click(models.Model):
+ result = models.ForeignKey(ResultURL, related_name='clicks')
+ datetime = models.DateTimeField()
+
+ def __unicode__(self):
+ return self.datetime.strftime('%B %d, %Y %H:%M:%S')
+
+ def get_weight(self, default=1, weighted=lambda value, days: value/days**2):
+ if not hasattr(self, '_weight'):
+ days = (datetime.datetime.now() - self.datetime).days
+ if days < 0:
+ raise ValueError("Click dates must be in the past.")
+ default = float(default)
+ if days == 0:
+ self._weight = float(default)
+ else:
+ self._weight = weighted(default, days)
+ return self._weight
+ weight = property(get_weight)
+
+ def clean(self):
+ if self.datetime > datetime.datetime.now():
+ raise ValidationError("Click dates must be in the past.")
+
+ class Meta:
+ ordering = ['datetime']
+ get_latest_by = 'datetime'
+
+
+class SearchView(MultiView):
+ results_page = models.ForeignKey(Page, related_name='search_results_related')
+ searches = SlugMultipleChoiceField(choices=registry.iterchoices())
+ enable_ajax_api = models.BooleanField("Enable AJAX API", default=True)
+ placeholder_text = models.CharField(max_length=75, default="Search")
+
+ search_form = SearchForm
+
+ def __unicode__(self):
+ return u"%s (%s)" % (self.placeholder_text, u", ".join([display for slug, display in registry.iterchoices()]))
+
+ def get_reverse_params(self, obj):
+ raise ViewCanNotProvideSubpath
+
+ @property
+ def urlpatterns(self):
+ urlpatterns = patterns('',
+ url(r'^$', self.results_view, name='results'),
+ )
+ if self.enable_ajax_api:
+ urlpatterns += patterns('',
+ url(r'^(?P<slug>[\w-]+)$', self.ajax_api_view, name='ajax_api_view')
+ )
+ return urlpatterns
+
+ def get_search_instance(self, slug, search_string):
+ return registry[slug](search_string.lower())
+
+ def results_view(self, request, extra_context=None):
+ results = None
+
+ context = self.get_context()
+ context.update(extra_context or {})
+
+ if SEARCH_ARG_GET_KEY in request.GET:
+ form = self.search_form(request.GET)
+
+ if form.is_valid():
+ search_string = request.GET[SEARCH_ARG_GET_KEY].lower()
+ url = request.GET.get(URL_REDIRECT_GET_KEY)
+ hash = request.GET.get(HASH_REDIRECT_GET_KEY)
+
+ if url and hash:
+ if check_redirect_hash(hash, search_string, url):
+ # Create the necessary models
+ search = Search.objects.get_or_create(string=search_string)[0]
+ result_url = search.result_urls.get_or_create(url=url)[0]
+ result_url.clicks.create(datetime=datetime.datetime.now())
+ return HttpResponseRedirect(url)
+ else:
+ messages.add_message(request, messages.INFO, "The link you followed had been tampered with. Here are all the results for your search term instead!")
+ # TODO: Should search_string be escaped here?
+ return HttpResponseRedirect("%s?%s=%s" % (request.path, SEARCH_ARG_GET_KEY, search_string))
+ if not self.enable_ajax_api:
+ search_instances = []
+ if eventlet:
+ pool = eventlet.GreenPool()
+ for slug in self.searches:
+ search_instance = self.get_search_instance(slug, search_string)
+ search_instances.append(search_instance)
+ if eventlet:
+ pool.spawn_n(self.make_result_cache, search_instance)
+ else:
+ self.make_result_cache(search_instance)
+ if eventlet:
+ pool.waitall()
+ context.update({
+ 'searches': search_instances
+ })
+ else:
+ context.update({
+ 'searches': [{'verbose_name': verbose_name, 'url': self.reverse('ajax_api_view', kwargs={'slug': slug}, node=request.node)} for slug, verbose_name in registry.iterchoices()]
+ })
+ else:
+ form = SearchForm()
+
+ context.update({
+ 'form': form
+ })
+ return self.results_page.render_to_response(request, extra_context=context)
+
+ def make_result_cache(self, search_instance):
+ search_instance.results
+
+ def ajax_api_view(self, request, slug, extra_context=None):
+ search_string = request.GET.get(SEARCH_ARG_GET_KEY)
+
+ if not request.is_ajax() or not self.enable_ajax_api or slug not in self.searches or search_string is None:
+ raise Http404
+
+ search_instance = self.get_search_instance(slug, search_string)
+ response = HttpResponse(json.dumps({
+ 'results': [result.get_context() for result in search_instance.results],
+ }))
+ return response
\ No newline at end of file
--- /dev/null
+#encoding: utf-8
+
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.core.cache import cache
+from django.db.models.options import get_verbose_name as convert_camelcase
+from django.utils import simplejson as json
+from django.utils.http import urlquote_plus
+from django.utils.safestring import mark_safe
+from django.utils.text import capfirst
+from django.template import loader, Context, Template
+import datetime
+from philo.contrib.sobol.utils import make_tracking_querydict
+
+try:
+ from eventlet.green import urllib2
+except:
+ import urllib2
+
+
+__all__ = (
+ 'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'registry'
+)
+
+
+SEARCH_CACHE_KEY = 'philo_sobol_search_results'
+DEFAULT_RESULT_TEMPLATE_STRING = "{% if url %}<a href='{{ url }}'>{% endif %}{{ title }}{% if url %}</a>{% endif %}"
+
+# Determines the timeout on the entire result cache.
+MAX_CACHE_TIMEOUT = 60*60*24*7
+
+
+class RegistrationError(Exception):
+ pass
+
+
+class SearchRegistry(object):
+ # Holds a registry of search types by slug.
+ def __init__(self):
+ self._registry = {}
+
+ def register(self, search, slug=None):
+ slug = slug or search.slug
+ if slug in self._registry:
+ if self._registry[slug] != search:
+ raise RegistrationError("A different search is already registered as `%s`")
+ else:
+ self._registry[slug] = search
+
+ def unregister(self, search, slug=None):
+ if slug is not None:
+ if slug in self._registry and self._registry[slug] == search:
+ del self._registry[slug]
+ raise RegistrationError("`%s` is not registered as `%s`" % (search, slug))
+ else:
+ for slug, search in self._registry.items():
+ if search == search:
+ del self._registry[slug]
+
+ def items(self):
+ return self._registry.items()
+
+ def iteritems(self):
+ return self._registry.iteritems()
+
+ def iterchoices(self):
+ for slug, search in self.iteritems():
+ yield slug, search.verbose_name
+
+ def __getitem__(self, key):
+ return self._registry[key]
+
+ def __iter__(self):
+ return self._registry.__iter__()
+
+
+registry = SearchRegistry()
+
+
+class Result(object):
+ """
+ A result is instantiated with a configuration dictionary, a search,
+ and a template name. The configuration dictionary is expected to
+ define a `title` and optionally a `url`. Any other variables may be
+ defined; they will be made available through the result object in
+ the template, if one is defined.
+ """
+ def __init__(self, search, result):
+ self.search = search
+ self.result = result
+
+ def get_title(self):
+ return self.search.get_result_title(self.result)
+
+ def get_url(self):
+ return "?%s" % self.search.get_result_querydict(self.result).urlencode()
+
+ def get_template(self):
+ return self.search.get_result_template(self.result)
+
+ def get_extra_context(self):
+ return self.search.get_result_extra_context(self.result)
+
+ def get_context(self):
+ context = self.get_extra_context()
+ context.update({
+ 'title': self.get_title(),
+ 'url': self.get_url()
+ })
+ return context
+
+ def render(self):
+ t = self.get_template()
+ c = Context(self.get_context())
+ return t.render(c)
+
+ def __unicode__(self):
+ return self.render()
+
+
+class BaseSearchMetaclass(type):
+ def __new__(cls, name, bases, attrs):
+ if 'verbose_name' not in attrs:
+ attrs['verbose_name'] = capfirst(convert_camelcase(name))
+ if 'slug' not in attrs:
+ attrs['slug'] = name.lower()
+ return super(BaseSearchMetaclass, cls).__new__(cls, name, bases, attrs)
+
+
+class BaseSearch(object):
+ """
+ Defines a generic search interface. Accessing self.results will
+ attempt to retrieve cached results and, if that fails, will
+ initiate a new search and store the results in the cache.
+ """
+ __metaclass__ = BaseSearchMetaclass
+ result_limit = 10
+ _cache_timeout = 60*48
+
+ def __init__(self, search_arg):
+ self.search_arg = search_arg
+
+ def _get_cached_results(self):
+ """Return the cached results if the results haven't timed out. Otherwise return None."""
+ result_cache = cache.get(SEARCH_CACHE_KEY)
+ if result_cache and self.__class__ in result_cache and self.search_arg.lower() in result_cache[self.__class__]:
+ cached = result_cache[self.__class__][self.search_arg.lower()]
+ if cached['timeout'] >= datetime.datetime.now():
+ return cached['results']
+ return None
+
+ def _set_cached_results(self, results, timeout):
+ """Sets the results to the cache for <timeout> minutes."""
+ result_cache = cache.get(SEARCH_CACHE_KEY) or {}
+ cached = result_cache.setdefault(self.__class__, {}).setdefault(self.search_arg.lower(), {})
+ cached.update({
+ 'results': results,
+ 'timeout': datetime.datetime.now() + datetime.timedelta(minutes=timeout)
+ })
+ cache.set(SEARCH_CACHE_KEY, result_cache, MAX_CACHE_TIMEOUT)
+
+ @property
+ def results(self):
+ if not hasattr(self, '_results'):
+ results = self._get_cached_results()
+ if results is None:
+ try:
+ # Cache one extra result so we can see if there are
+ # more results to be had.
+ limit = self.result_limit
+ if limit is not None:
+ limit += 1
+ results = self.get_results(self.result_limit)
+ except:
+ if settings.DEBUG:
+ raise
+ # On exceptions, don't set any cache; just return.
+ return []
+
+ self._set_cached_results(results, self._cache_timeout)
+ self._results = results
+
+ return self._results
+
+ def get_results(self, limit=None, result_class=Result):
+ """
+ Calls self.search() and parses the return value into Result objects.
+ """
+ results = self.search(limit)
+ return [result_class(self, result) for result in results]
+
+ def search(self, limit=None):
+ """
+ Returns an iterable of up to <limit> results. The
+ get_result_title, get_result_url, get_result_template, and
+ get_result_extra_context methods will be used to interpret the
+ individual items that this function returns, so the result can
+ be an object with attributes as easily as a dictionary
+ with keys. The only restriction is that the objects be
+ pickleable so that they can be used with django's cache system.
+ """
+ raise NotImplementedError
+
+ def get_result_title(self, result):
+ raise NotImplementedError
+
+ def get_result_url(self, result):
+ "Subclasses override this to provide the actual URL for the result."
+ raise NotImplementedError
+
+ def get_result_querydict(self, result):
+ return make_tracking_querydict(self.search_arg, self.get_result_url(result))
+
+ def get_result_template(self, result):
+ if hasattr(self, 'result_template'):
+ return loader.get_template(self.result_template)
+ if not hasattr(self, '_result_template'):
+ self._result_template = Template(DEFAULT_RESULT_TEMPLATE_STRING)
+ return self._result_template
+
+ def get_result_extra_context(self, result):
+ return {}
+
+ def has_more_results(self):
+ """Useful to determine whether to display a `view more results` link."""
+ return len(self.results) > self.result_limit
+
+ @property
+ def more_results_url(self):
+ """
+ Returns the actual url for more results. This will be encoded
+ into a querystring for tracking purposes.
+ """
+ raise NotImplementedError
+
+ @property
+ def more_results_querydict(self):
+ return make_tracking_querydict(self.search_arg, self.more_results_url)
+
+ def __unicode__(self):
+ return ' '.join(self.__class__.verbose_name.rsplit(' ', 1)[:-1]) + ' results'
+
+
+class DatabaseSearch(BaseSearch):
+ model = None
+
+ def has_more_results(self):
+ return self.get_queryset().count() > self.result_limit
+
+ def search(self, limit=None):
+ if not hasattr(self, '_qs'):
+ self._qs = self.get_queryset()
+ if limit is not None:
+ self._qs = self._qs[:limit]
+
+ return self._qs
+
+ def get_queryset(self):
+ return self.model._default_manager.all()
+
+
+class URLSearch(BaseSearch):
+ """
+ Defines a generic interface for searches that require accessing a
+ certain url to get search results.
+ """
+ search_url = ''
+ query_format_str = "%s"
+
+ @property
+ def url(self):
+ "The URL where the search gets its results."
+ return self.search_url + self.query_format_str % urlquote_plus(self.search_arg)
+
+ @property
+ def more_results_url(self):
+ "The URL where the users would go to get more results."
+ return self.url
+
+ def parse_response(self, response, limit=None):
+ raise NotImplementedError
+
+ def search(self, limit=None):
+ return self.parse_response(urllib2.urlopen(self.url), limit=limit)
+
+
+class JSONSearch(URLSearch):
+ """
+ Makes a GET request and parses the results as JSON. The default
+ behavior assumes that the return value is a list of results.
+ """
+ def parse_response(self, response, limit=None):
+ return json.loads(response.read())[:limit]
+
+
+class GoogleSearch(JSONSearch):
+ search_url = "http://ajax.googleapis.com/ajax/services/search/web"
+ query_format_str = "?v=1.0&q=%s"
+ # TODO: Change this template to reflect the app's actual name.
+ result_template = 'search/googlesearch.html'
+ timeout = 60
+
+ def parse_response(self, response, limit=None):
+ responseData = json.loads(response.read())['responseData']
+ results, cursor = responseData['results'], responseData['cursor']
+
+ if results:
+ self._more_results_url = cursor['moreResultsUrl']
+ self._estimated_result_count = cursor['estimatedResultCount']
+
+ return results[:limit]
+
+ @property
+ def url(self):
+ # Google requires that an ajax request have a proper Referer header.
+ return urllib2.Request(
+ super(GoogleSearch, self).url,
+ None,
+ {'Referer': "http://%s" % Site.objects.get_current().domain}
+ )
+
+ @property
+ def has_more_results(self):
+ if self.results and len(self.results) < self._estimated_result_count:
+ return True
+ return False
+
+ @property
+ def more_results_url(self):
+ return self._more_results_url
+
+ def get_result_title(self, result):
+ return result['titleNoFormatting']
+
+ def get_result_url(self, result):
+ return result['unescapedUrl']
+
+ def get_result_extra_context(self, result):
+ return result
+
+
+registry.register(GoogleSearch)
+
+
+try:
+ from BeautifulSoup import BeautifulSoup, SoupStrainer, BeautifulStoneSoup
+except:
+ pass
+else:
+ __all__ += ('ScrapeSearch', 'XMLSearch',)
+ class ScrapeSearch(URLSearch):
+ _strainer_args = []
+ _strainer_kwargs = {}
+
+ @property
+ def strainer(self):
+ if not hasattr(self, '_strainer'):
+ self._strainer = SoupStrainer(*self._strainer_args, **self._strainer_kwargs)
+ return self._strainer
+
+ def parse_response(self, response, limit=None):
+ strainer = self.strainer
+ soup = BeautifulSoup(response, parseOnlyThese=strainer)
+ return self.parse_results(soup[:limit])
+
+ def parse_results(self, results):
+ """
+ Provides a hook for parsing the results of straining. This
+ has no default behavior because the results absolutely
+ must be parsed to properly extract the information.
+ For more information, see http://www.crummy.com/software/BeautifulSoup/documentation.html#Improving%20Memory%20Usage%20with%20extract
+ """
+ raise NotImplementedError
+
+
+ class XMLSearch(ScrapeSearch):
+ _self_closing_tags = []
+
+ def parse_response(self, response, limit=None):
+ strainer = self.strainer
+ soup = BeautifulStoneSoup(page, selfClosingTags=self._self_closing_tags, parseOnlyThese=strainer)
+ return self.parse_results(soup[:limit])
\ No newline at end of file
--- /dev/null
+{% extends "admin/base_site.html" %}
+
+<!-- LOADING -->
+{% load i18n %}
+
+<!-- EXTRASTYLES -->
+{% block extrastyle %}<style type="text/css">.favored{font-weight:bold;}</style>{% endblock %}
+
+<!-- BREADCRUMBS -->
+{% block breadcrumbs %}
+ <div id="breadcrumbs">
+ {% if queryset|length > 1 %}
+ <a href="../../">{% trans "Home" %}</a> ›
+ <a href="../">{{ app_label|capfirst }}</a> ›
+ <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> ›
+ {% trans 'Search results for multiple objects' %}
+ {% else %}
+ <a href="../../../../">{% trans "Home" %}</a> ›
+ <a href="../../../">{{ app_label|capfirst }}</a> ›
+ <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> ›
+ <a href="../">{{ queryset|first|truncatewords:"18" }}</a> ›
+ {% trans 'Results' %}
+ {% endif %}
+ </div>
+{% endblock %}
+
+<!-- CONTENT -->
+{% block content %}
+ <div class="container-grid delete-confirmation">
+ {% for search in queryset %}
+ {% if not forloop.first and not forloop.last %}<h1>{{ search.string }}</h1>{% endif %}
+ <div class="group tabular">
+ <h2>{% blocktrans %}Results{% endblocktrans %}</h2>{% comment %}For the favored results, add a class?{% endcomment %}
+ <div class="module table">
+ <div class="module thead">
+ <div class="tr">
+ <div class="th">Weight</div>
+ <div class="th">URL</div>
+ </div>
+ </div>
+ <div class="module tbody">
+ {% for result in search.get_weighted_results %}
+ <div class="tr{% if result in search.get_favored_results %} favored{% endif %}">
+ <div class="td">{{ result.weight }}</div>
+ <div class="td">{{ result.url }}</div>
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+{% endblock %}
\ No newline at end of file
--- /dev/null
+{% extends "admin/base_site.html" %}
+{% load i18n %}
+
+{% block extrastyle %}<style type="text/css">.favored{font-weight:bold;}</style>{% endblock %}
+
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+ {% if queryset|length > 1 %}
+ <a href="../../">{% trans "Home" %}</a> ›
+ <a href="../">{{ app_label|capfirst }}</a> ›
+ <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> ›
+ {% trans 'Search results for multiple objects' %}
+ {% else %}
+ <a href="../../../../">{% trans "Home" %}</a> ›
+ <a href="../../../">{{ app_label|capfirst }}</a> ›
+ <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> ›
+ <a href="../">{{ queryset|first|truncatewords:"18" }}</a> ›
+ {% trans 'Results' %}
+ {% endif %}
+</div>
+{% endblock %}
+
+
+{% block content %}
+ {% for search in queryset %}
+ {% if not forloop.first and not forloop.last %}<h1>{{ search.string }}</h1>{% endif %}
+ <fieldset class="module">
+ <h2>{% blocktrans %}Results{% endblocktrans %}</h2>{% comment %}For the favored results, add a class?{% endcomment %}
+ <table>
+ <thead>
+ <tr>
+ <th>Weight</th>
+ <th>URL</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for result in search.get_weighted_results %}
+ <tr{% if result in search.favored_results %} class="favored"{% endif %}>
+ <td>{{ result.weight }}</td>
+ <td>{{ result.url }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </fieldset>
+ {% endfor %}
+{% endblock %}
\ No newline at end of file
--- /dev/null
+<article>
+ <h1><a href="{{ url }}">{{ title|safe }}</a></h1>
+ <p>{{ content|safe }}</p>
+</article>
\ No newline at end of file
--- /dev/null
+from django.conf import settings
+from django.http import QueryDict
+from django.utils.encoding import smart_str
+from django.utils.hashcompat import sha_constructor
+from django.utils.http import urlquote_plus, urlquote
+
+
+SEARCH_ARG_GET_KEY = 'q'
+URL_REDIRECT_GET_KEY = 'url'
+HASH_REDIRECT_GET_KEY = 's'
+
+
+def make_redirect_hash(search_arg, url):
+ return sha_constructor(smart_str(search_arg + url + settings.SECRET_KEY)).hexdigest()[::2]
+
+
+def check_redirect_hash(hash, search_arg, url):
+ return hash == make_redirect_hash(search_arg, url)
+
+
+def make_tracking_querydict(search_arg, url):
+ """
+ Returns a QueryDict instance containing the information necessary
+ for tracking clicks of this url.
+
+ NOTE: will this kind of initialization handle quoting correctly?
+ """
+ return QueryDict("%s=%s&%s=%s&%s=%s" % (
+ SEARCH_ARG_GET_KEY, urlquote_plus(search_arg),
+ URL_REDIRECT_GET_KEY, urlquote(url),
+ HASH_REDIRECT_GET_KEY, make_redirect_hash(search_arg, url))
+ )
\ No newline at end of file
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 import forms
+from django.core.exceptions import ValidationError
+from django.core.validators import validate_slug
from django.db import models
from django.utils import simplejson as json
+from django.utils.text import capfirst
+from django.utils.translation import ugettext_lazy as _
from philo.forms.fields import JSONFormField
from philo.validators import TemplateValidator, json_validator
#from philo.models.fields.entities import *
return super(JSONField, self).formfield(*args, **kwargs)
+class SlugMultipleChoiceField(models.Field):
+ __metaclass__ = models.SubfieldBase
+ description = _("Comma-separated slug field")
+
+ def get_internal_type(self):
+ return "TextField"
+
+ def to_python(self, value):
+ if not value:
+ return []
+
+ if isinstance(value, list):
+ return value
+
+ return value.split(',')
+
+ def get_prep_value(self, value):
+ return ','.join(value)
+
+ def formfield(self, **kwargs):
+ # This is necessary because django hard-codes TypedChoiceField for things with choices.
+ defaults = {
+ 'widget': forms.CheckboxSelectMultiple,
+ 'choices': self.get_choices(include_blank=False),
+ 'label': capfirst(self.verbose_name),
+ 'required': not self.blank,
+ 'help_text': self.help_text
+ }
+ if self.has_default():
+ if callable(self.default):
+ defaults['initial'] = self.default
+ defaults['show_hidden_initial'] = True
+ else:
+ defaults['initial'] = self.get_default()
+
+ for k in kwargs.keys():
+ if k not in ('coerce', 'empty_value', 'choices', 'required',
+ 'widget', 'label', 'initial', 'help_text',
+ 'error_messages', 'show_hidden_initial'):
+ del kwargs[k]
+
+ defaults.update(kwargs)
+ form_class = forms.TypedMultipleChoiceField
+ return form_class(**defaults)
+
+ def validate(self, value, model_instance):
+ invalid_values = []
+ for val in value:
+ try:
+ validate_slug(val)
+ except ValidationError:
+ invalid_values.append(val)
+
+ if invalid_values:
+ # should really make a custom message.
+ raise ValidationError(self.error_messages['invalid_choice'] % invalid_values)
+
+
try:
from south.modelsinspector import add_introspection_rules
except ImportError:
pass
else:
+ add_introspection_rules([], ["^philo\.models\.fields\.SlugMultipleChoiceField"])
add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"])
add_introspection_rules([], ["^philo\.models\.fields\.JSONField"])
\ No newline at end of file
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
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.")
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 = {}
- for key, val in params.items():
- if isinstance(key, unicode):
- key = str(key)
- kwargs[key] = val
+ 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