opts = form._meta
if issubclass(form, EntityForm) and opts.model:
proxy_fields = proxy_fields_for_entity_model(opts.model).keys()
+
+ # Store readonly fields iff they have been declared.
+ if 'readonly_fields' in attrs or not hasattr(new_class, '_real_readonly_fields'):
+ new_class._real_readonly_fields = new_class.readonly_fields
+
readonly_fields = new_class.readonly_fields
- new_class._real_readonly_fields = readonly_fields
- new_class.readonly_fields = list(readonly_fields) + proxy_fields
+ new_class.readonly_fields = list(set(readonly_fields) | set(proxy_fields))
# Additional HACKS to handle raw_id_fields and other attributes that the admin
# uses model._meta.get_field to validate.
from django.contrib import admin
-from philo.admin.base import EntityAdmin, TreeEntityAdmin
+from philo.admin.base import EntityAdmin, TreeEntityAdmin, COLLAPSE_CLASSES
from philo.models import Node, Redirect, File
class NodeAdmin(TreeEntityAdmin):
list_display = ('slug', 'view', 'accepts_subpath')
+ related_lookup_fields = {
+ 'fk': [],
+ 'm2m': [],
+ 'generic': [['view_content_type', 'view_object_id']]
+ }
def accepts_subpath(self, obj):
return obj.accepts_subpath
class RedirectAdmin(ViewAdmin):
fieldsets = (
(None, {
- 'fields': ('target', 'status_code')
+ 'fields': ('target_node', 'url_or_subpath', 'status_code')
}),
+ ('Advanced', {
+ 'fields': ('reversing_parameters',),
+ 'classes': COLLAPSE_CLASSES
+ })
)
- list_display = ('target', 'status_code')
+ list_display = ('target_url', 'status_code', 'target_node', 'url_or_subpath')
list_filter = ('status_code',)
+ raw_id_fields = ['target_node']
+ related_lookup_fields = {
+ 'fk': raw_id_fields
+ }
class FileAdmin(ViewAdmin):
'classes': COLLAPSE_CLASSES
})
)
+ related_lookup_fields = {'fk': raw_id_fields}
class BlogViewAdmin(EntityAdmin):
('Archive Pages', {
'fields': ('entry_archive_page', 'tag_archive_page')
}),
- ('Permalinks', {
- 'fields': ('entry_permalink_style', 'entry_permalink_base', 'tag_permalink_base'),
+ ('General Settings', {
+ 'fields': ('entry_permalink_style', 'entry_permalink_base', 'tag_permalink_base', 'entries_per_page'),
'classes': COLLAPSE_CLASSES
}),
- ('Feeds', {
- 'fields': ('feed_suffix', 'feeds_enabled'),
+ ('Feed Settings', {
+ 'fields': ( 'feeds_enabled', 'feed_suffix', 'feed_type', 'item_title_template', 'item_description_template',),
'classes': COLLAPSE_CLASSES
})
)
- raw_id_fields = ('index_page', 'entry_page', 'tag_page', 'entry_archive_page', 'tag_archive_page',)
+ raw_id_fields = ('index_page', 'entry_page', 'tag_page', 'entry_archive_page', 'tag_archive_page', 'item_title_template', 'item_description_template',)
+ related_lookup_fields = {'fk': raw_id_fields}
class NewsletterAdmin(TitledAdmin):
'classes': COLLAPSE_CLASSES
}),
('Feeds', {
- 'fields': ('feed_suffix', 'feeds_enabled'),
+ 'fields': ( 'feeds_enabled', 'feed_suffix', 'feed_type', 'item_title_template', 'item_description_template',),
'classes': COLLAPSE_CLASSES
})
)
- raw_id_fields = ('index_page', 'article_page', 'issue_page', 'article_archive_page', 'issue_archive_page',)
+ raw_id_fields = ('index_page', 'article_page', 'issue_page', 'article_archive_page', 'issue_archive_page', 'item_title_template', 'item_description_template',)
+ related_lookup_fields = {'fk': raw_id_fields}
admin.site.register(Blog, BlogAdmin)
--- /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_type'
+ 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)
+
+ # Adding field 'NewsletterView.item_description_template'
+ db.add_column('penfield_newsletterview', 'item_description_template', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='penfield_newsletterview_description_related', null=True, to=orm['philo.Template']), keep_default=False)
+
+ # Adding field 'BlogView.feed_type'
+ db.add_column('penfield_blogview', 'feed_type', self.gf('django.db.models.fields.CharField')(default='atom', max_length=50), keep_default=False)
+
+ # Adding field 'BlogView.item_title_template'
+ db.add_column('penfield_blogview', 'item_title_template', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='penfield_blogview_title_related', null=True, to=orm['philo.Template']), keep_default=False)
+
+ # Adding field 'BlogView.item_description_template'
+ db.add_column('penfield_blogview', 'item_description_template', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='penfield_blogview_description_related', null=True, to=orm['philo.Template']), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'NewsletterView.feed_type'
+ db.delete_column('penfield_newsletterview', 'feed_type')
+
+ # Deleting field 'NewsletterView.item_title_template'
+ db.delete_column('penfield_newsletterview', 'item_title_template_id')
+
+ # Deleting field 'NewsletterView.item_description_template'
+ db.delete_column('penfield_newsletterview', 'item_description_template_id')
+
+ # Deleting field 'BlogView.feed_type'
+ db.delete_column('penfield_blogview', 'feed_type')
+
+ # Deleting field 'BlogView.item_title_template'
+ db.delete_column('penfield_blogview', 'item_title_template_id')
+
+ # Deleting field 'BlogView.item_description_template'
+ db.delete_column('penfield_blogview', 'item_description_template_id')
+
+
+ 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_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'atom'", '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_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'atom'", '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', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ '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', [], {'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.conf import settings
from django.conf.urls.defaults import url, patterns, include
+from django.contrib.sites.models import Site, RequestSite
+from django.contrib.syndication.views import add_domain
from django.db import models
-from django.http import Http404
-from django.template import loader, Context
-from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
+from django.http import Http404, HttpResponse
+from django.template import RequestContext, Template as DjangoTemplate
+from django.utils import feedgenerator, tzinfo
+from django.utils.datastructures import SortedDict
+from django.utils.encoding import smart_unicode, force_unicode
+from django.utils.html import escape
from datetime import date, datetime
-from philo.contrib.penfield.utils import FeedMultiViewMixin
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
+from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField, Template
from philo.utils import paginate
+try:
+ import mimeparse
+except:
+ mimeparse = None
+
+
+ATOM = feedgenerator.Atom1Feed.mime_type
+RSS = feedgenerator.Rss201rev2Feed.mime_type
+FEEDS = SortedDict([
+ (ATOM, feedgenerator.Atom1Feed),
+ (RSS, feedgenerator.Rss201rev2Feed),
+])
+FEED_CHOICES = (
+ (ATOM, "Atom"),
+ (RSS, "RSS"),
+)
+
+
+class FeedView(MultiView):
+ """
+ The FeedView expects to handle a number of different feeds for the
+ same object - i.e. patterns for a blog to handle all entries or
+ just entries for a certain year/month/day.
+
+ This class would subclass django.contrib.syndication.views.Feed, but
+ that would make it callable, which causes problems.
+ """
+ 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)
+
+ 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")
+
+ item_context_var = 'items'
+ object_attr = 'object'
+
+ description = ""
+
+ 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('')
+ if self.feeds_enabled:
+ feed_reverse_name = "%s_feed" % reverse_name
+ feed_view = 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),
+ )
+ 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):
+ return getattr(self, self.object_attr)
+
+ def feed_view(self, get_items_attr, reverse_name):
+ """
+ Returns a view function that renders a list of items as a feed.
+ """
+ get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
+
+ def inner(request, extra_context=None, *args, **kwargs):
+ obj = self.get_object(request, *args, **kwargs)
+ feed = self.get_feed(obj, request, reverse_name)
+ items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
+ self.populate_feed(feed, items, request)
+
+ response = HttpResponse(mimetype=feed.mime_type)
+ feed.write(response, 'utf-8')
+ return response
+
+ return inner
+
+ def page_view(self, get_items_attr, page_attr):
+ """
+ Returns a view function that renders a list of items as a page.
+ """
+ get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
+ page = isinstance(page_attr, Page) and page_attr or getattr(self, page_attr)
+
+ def inner(request, extra_context=None, *args, **kwargs):
+ items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
+ items, item_context = self.process_page_items(request, items)
+
+ context = self.get_context()
+ context.update(extra_context or {})
+ context.update(item_context or {})
+
+ return page.render_to_response(request, extra_context=context)
+ return inner
+
+ def process_page_items(self, request, items):
+ """
+ Hook for handling any extra processing of items based on a
+ request, such as pagination or searching. This method is
+ expected to return a list of items and a dictionary to be
+ added to the page context.
+ """
+ item_context = {
+ self.item_context_var: items
+ }
+ return items, item_context
+
+ 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?
+ if mimeparse:
+ feed_type = mimeparse.best_match(FEEDS.keys(), accept)
+ else:
+ for feed_type in FEEDS.keys():
+ if feed_type in accept or "%s/*" % feed_type.split("/")[0] in accept:
+ break
+ else:
+ feed_type = None
+ if not feed_type:
+ # See RFC 2616
+ return HttpResponse(status=406)
+ return FEEDS[feed_type]
+
+ def get_feed(self, obj, request, reverse_name):
+ """
+ Returns an unpopulated feedgenerator.DefaultFeed object for this object.
+ """
+ try:
+ current_site = Site.objects.get_current()
+ except Site.DoesNotExist:
+ current_site = RequestSite(request)
+
+ feed_type = self.get_feed_type(request)
+ node = request.node
+ link = node.get_absolute_url(with_domain=True, request=request, secure=request.is_secure())
+
+ feed = feed_type(
+ title = self.__get_dynamic_attr('title', obj),
+ subtitle = self.__get_dynamic_attr('subtitle', obj),
+ link = link,
+ description = self.__get_dynamic_attr('description', obj),
+ language = settings.LANGUAGE_CODE.decode(),
+ feed_url = add_domain(
+ current_site.domain,
+ self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node.subpath, with_domain=True, request=request, secure=request.is_secure()),
+ request.is_secure()
+ ),
+ author_name = self.__get_dynamic_attr('author_name', obj),
+ author_link = self.__get_dynamic_attr('author_link', obj),
+ author_email = self.__get_dynamic_attr('author_email', obj),
+ categories = self.__get_dynamic_attr('categories', obj),
+ feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
+ feed_guid = self.__get_dynamic_attr('feed_guid', obj),
+ ttl = self.__get_dynamic_attr('ttl', obj),
+ **self.feed_extra_kwargs(obj)
+ )
+ return feed
+
+ def populate_feed(self, feed, items, request):
+ if self.item_title_template:
+ title_template = DjangoTemplate(self.item_title_template.code)
+ else:
+ title_template = None
+ if self.item_description_template:
+ description_template = DjangoTemplate(self.item_description_template.code)
+ else:
+ description_template = None
+
+ node = request.node
+ try:
+ current_site = Site.objects.get_current()
+ except Site.DoesNotExist:
+ current_site = RequestSite(request)
+
+ for item in items:
+ if title_template is not None:
+ title = title_template.render(RequestContext(request, {'obj': item}))
+ else:
+ title = self.__get_dynamic_attr('item_title', item)
+ if description_template is not None:
+ description = description_template.render(RequestContext(request, {'obj': item}))
+ else:
+ description = self.__get_dynamic_attr('item_description', item)
+
+ link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
+
+ enc = None
+ enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
+ if enc_url:
+ enc = feedgenerator.Enclosure(
+ url = smart_unicode(add_domain(
+ current_site.domain,
+ enc_url,
+ request.is_secure()
+ )),
+ length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
+ mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
+ )
+ author_name = self.__get_dynamic_attr('item_author_name', item)
+ if author_name is not None:
+ author_email = self.__get_dynamic_attr('item_author_email', item)
+ author_link = self.__get_dynamic_attr('item_author_link', item)
+ else:
+ author_email = author_link = None
+
+ pubdate = self.__get_dynamic_attr('item_pubdate', item)
+ if pubdate and not pubdate.tzinfo:
+ ltz = tzinfo.LocalTimezone(pubdate)
+ pubdate = pubdate.replace(tzinfo=ltz)
+
+ feed.add_item(
+ title = title,
+ link = link,
+ description = description,
+ unique_id = self.__get_dynamic_attr('item_guid', item, link),
+ enclosure = enc,
+ pubdate = pubdate,
+ author_name = author_name,
+ author_email = author_email,
+ author_link = author_link,
+ categories = self.__get_dynamic_attr('item_categories', item),
+ item_copyright = self.__get_dynamic_attr('item_copyright', item),
+ **self.item_extra_kwargs(item)
+ )
+
+ def __get_dynamic_attr(self, attname, obj, default=None):
+ try:
+ attr = getattr(self, attname)
+ except AttributeError:
+ return default
+ if callable(attr):
+ # Check func_code.co_argcount rather than try/excepting the
+ # function and catching the TypeError, because something inside
+ # the function may raise the TypeError. This technique is more
+ # accurate.
+ if hasattr(attr, 'func_code'):
+ argcount = attr.func_code.co_argcount
+ else:
+ argcount = attr.__call__.func_code.co_argcount
+ if argcount == 2: # one argument is 'self'
+ return attr(obj)
+ else:
+ return attr()
+ return attr
+
+ def feed_extra_kwargs(self, obj):
+ """
+ Returns an extra keyword arguments dictionary that is used when
+ initializing the feed generator.
+ """
+ return {}
+
+ def item_extra_kwargs(self, item):
+ """
+ Returns an extra keyword arguments dictionary that is used with
+ the `add_item` call of the feed generator.
+ """
+ return {}
+
+ def item_title(self, item):
+ return escape(force_unicode(item))
+
+ def item_description(self, item):
+ return force_unicode(item)
+
+ class Meta:
+ abstract=True
class Blog(Entity, Titled):
register_value_model(BlogEntry)
-class BlogView(MultiView, FeedMultiViewMixin):
+class BlogView(FeedView):
ENTRY_PERMALINK_STYLE_CHOICES = (
('D', 'Year, month, and day'),
('M', 'Year and month'),
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)
entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
- feed_suffix = models.CharField(max_length=255, blank=False, default=FeedMultiViewMixin.feed_suffix)
- feeds_enabled = models.BooleanField()
- list_var = 'entries'
+
+ item_context_var = 'entries'
+ object_attr = 'blog'
def __unicode__(self):
return u'BlogView for %s' % self.blog.title
- @property
- def per_page(self):
- return self.entries_per_page
-
def get_reverse_params(self, obj):
if isinstance(obj, BlogEntry):
if obj.blog == self.blog:
if self.entry_permalink_style == 'D':
kwargs.update({'day': str(obj.date.day).zfill(2)})
return self.entry_view, [], kwargs
- elif isinstance(obj, Tag):
- if obj in self.get_tag_queryset():
- return 'entries_by_tag', [], {'tag_slugs': obj.slug}
+ 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()]
+ if slugs:
+ return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)}
elif isinstance(obj, (date, datetime)):
kwargs = {
'year': str(obj.year).zfill(4),
@property
def urlpatterns(self):
- urlpatterns = patterns('',
- url(r'^', include(self.feed_patterns(self.get_all_entries, self.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(self.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(self.get_entries_by_tag, self.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, 'tag_archive')
+ 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(self.get_entries_by_ymd, self.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(self.get_entries_by_ymd, self.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(self.get_entries_by_ymd, self.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('',
- url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)/?$', self.entry_view)
+ url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
)
elif self.entry_permalink_style == 'M':
urlpatterns += patterns('',
- url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)/?$', self.entry_view)
+ url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
)
elif self.entry_permalink_style == 'Y':
urlpatterns += patterns('',
- url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)/?$', self.entry_view)
+ url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)$', self.entry_view)
)
elif self.entry_permalink_style == 'B':
urlpatterns += patterns('',
- url((r'^(?:%s)/(?P<slug>[-\w]+)/?$' % self.entry_permalink_base), self.entry_view)
+ url((r'^%s/(?P<slug>[-\w]+)$' % self.entry_permalink_base), self.entry_view)
)
else:
urlpatterns = patterns('',
- url(r'^(?P<slug>[-\w]+)/?$', self.entry_view)
+ url(r'^(?P<slug>[-\w]+)$', self.entry_view)
)
return urlpatterns
})
return self.tag_archive_page.render_to_response(request, extra_context=context)
- def add_item(self, feed, obj, kwargs=None):
- title = loader.get_template("penfield/feeds/blog_entry/title.html")
- description = loader.get_template("penfield/feeds/blog_entry/description.html")
- defaults = {
- 'title': title.render(Context({'entry': obj})),
- 'description': description.render(Context({'entry': obj})),
- 'author_name': obj.author.get_full_name(),
- 'pubdate': obj.date
- }
- defaults.update(kwargs or {})
- super(BlogView, self).add_item(feed, obj, defaults)
-
- def get_feed(self, feed_type, extra_context, kwargs=None):
- tags = (extra_context or {}).get('tags', None)
- title = self.blog.title
+ def feed_view(self, get_items_attr, reverse_name):
+ get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
- if tags is not None:
- title += " - %s" % ', '.join([tag.name for tag in tags])
+ def inner(request, extra_context=None, *args, **kwargs):
+ obj = self.get_object(request, *args, **kwargs)
+ feed = self.get_feed(obj, request, reverse_name)
+ items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
+ self.populate_feed(feed, items, request)
+
+ if 'tags' in extra_context:
+ tags = extra_context['tags']
+ 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
+
+ feed.feed['categories'] = [tag.name for tag in tags]
+
+ response = HttpResponse(mimetype=feed.mime_type)
+ feed.write(response, 'utf-8')
+ return response
- defaults = {
- 'title': title
- }
- defaults.update(kwargs or {})
- return super(BlogView, self).get_feed(feed_type, extra_context, defaults)
+ return inner
+
+ def process_page_items(self, request, items):
+ if self.entries_per_page:
+ page_num = request.GET.get('page', 1)
+ paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num)
+ item_context = {
+ 'paginator': paginator,
+ 'paginated_page': paginated_page,
+ self.item_context_var: items
+ }
+ else:
+ item_context = {
+ self.item_context_var: items
+ }
+ return items, item_context
+
+ def title(self, obj):
+ return obj.title
+
+ def item_title(self, item):
+ return item.title
+
+ def item_description(self, item):
+ return item.content
+
+ def item_author_name(self, item):
+ return item.author.get_full_name()
+
+ def item_pubdate(self, item):
+ return item.date
+
+ def item_categories(self, item):
+ return [tag.name for tag in item.tags.all()]
class Newsletter(Entity, Titled):
register_value_model(NewsletterIssue)
-class NewsletterView(MultiView, FeedMultiViewMixin):
+class NewsletterView(FeedView):
ARTICLE_PERMALINK_STYLE_CHOICES = (
('D', 'Year, month, and day'),
('M', 'Year and month'),
article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
- feed_suffix = models.CharField(max_length=255, blank=False, default=FeedMultiViewMixin.feed_suffix)
- feeds_enabled = models.BooleanField()
- list_var = 'articles'
+ item_context_var = 'articles'
+ object_attr = 'newsletter'
def __unicode__(self):
- return self.newsletter.__unicode__()
+ return "NewsletterView for %s" % self.newsletter.__unicode__()
def get_reverse_params(self, obj):
if isinstance(obj, NewsletterArticle):
@property
def urlpatterns(self):
- urlpatterns = patterns('',
- url(r'^', include(self.feed_patterns(self.get_all_articles, self.index_page, 'index'))),
- url(r'^(?:%s)/(?P<numbering>.+)/' % self.issue_permalink_base, include(self.feed_patterns(self.get_articles_by_issue, self.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.issue_permalink_base, self.issue_archive_view, 'issue_archive')
+ url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
)
if self.article_archive_page:
urlpatterns += patterns('',
- url(r'^(?:%s)/' % self.article_permalink_base, include(self.feed_patterns(self.get_all_articles, self.article_archive_page, 'articles')))
+ 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(self.get_articles_by_ymd, self.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(self.get_articles_by_ymd, self.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(self.get_articles_by_ymd, self.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('',
- url(r'^(?:%s)/(?P<year>\d{4})/(?P<slug>[\w-]+)/$' % self.article_permalink_base, self.article_view)
+ url(r'^%s/(?P<year>\d{4})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
)
elif self.article_permalink_style == 'M':
urlpatterns += patterns('',
- url(r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w-]+)/$' % self.article_permalink_base, self.article_view)
+ url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
)
elif self.article_permalink_style == 'D':
urlpatterns += patterns('',
- url(r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)/$' % self.article_permalink_base, self.article_view)
+ url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
)
else:
urlpatterns += patterns('',
- url(r'^(?:%s)/(?P<slug>[-\w]+)/?$' % self.article_permalink_base, self.article_view)
+ url(r'^%s/(?P<slug>[-\w]+)$' % self.article_permalink_base, self.article_view)
)
return urlpatterns
})
return self.issue_archive_page.render_to_response(request, extra_context=context)
- def add_item(self, feed, obj, kwargs=None):
- title = loader.get_template("penfield/feeds/newsletter_article/title.html")
- description = loader.get_template("penfield/feeds/newsletter_article/description.html")
- defaults = {
- 'title': title.render(Context({'article': obj})),
- 'author_name': ', '.join([author.get_full_name() for author in obj.authors.all()]),
- 'pubdate': obj.date,
- 'description': description.render(Context({'article': obj})),
- 'categories': [tag.name for tag in obj.tags.all()]
- }
- defaults.update(kwargs or {})
- super(NewsletterView, self).add_item(feed, obj, defaults)
+ def title(self, obj):
+ return obj.title
- def get_feed(self, feed_type, extra_context, kwargs=None):
- title = self.newsletter.title
-
- defaults = {
- 'title': title
- }
- defaults.update(kwargs or {})
- return super(NewsletterView, self).get_feed(feed_type, extra_context, defaults)
+ def item_title(self, item):
+ return item.title
+
+ def item_description(self, item):
+ return item.full_text
+
+ def item_author_name(self, item):
+ authors = list(item.authors.all())
+ if len(authors) > 1:
+ return "%s and %s" % (", ".join([author.get_full_name() for author in authors[:-1]]), authors[-1].get_full_name())
+ elif authors:
+ return authors[0].get_full_name()
+ else:
+ return ''
+
+ def item_pubdate(self, item):
+ return item.date
+
+ def item_categories(self, item):
+ return [tag.name for tag in item.tags.all()]
\ No newline at end of file
+++ /dev/null
-{{ entry.content }}
\ No newline at end of file
+++ /dev/null
-{{ entry.title }}
\ No newline at end of file
+++ /dev/null
-{{ article.full_text }}
\ No newline at end of file
+++ /dev/null
-{{ article.title }}
\ No newline at end of file
+++ /dev/null
-from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
-from django.conf.urls.defaults import url, patterns
-from django.contrib.sites.models import Site
-from django.core.urlresolvers import reverse
-from django.http import HttpResponse
-from philo.utils import paginate
-
-
-class FeedMultiViewMixin(object):
- """
- This mixin provides common methods for adding feeds to multiviews. In order to use this mixin,
- the multiview must define feed_title (probably as properties that return values
- on related objects.) feed_description may also be defined; it defaults to an empty string.
- """
- feed_suffix = 'feed'
- feeds_enabled = True
- atom_feed = Atom1Feed
- rss_feed = Rss201rev2Feed
- feed_title = None
- feed_description = None
- list_var = 'objects'
-
- def page_view(self, func, page):
- """
- Wraps an object-fetching function and renders the results as a page.
- """
- def inner(request, extra_context=None, **kwargs):
- objects, extra_context = func(request=request, extra_context=extra_context, **kwargs)
-
- context = self.get_context()
- context.update(extra_context or {})
-
- if 'page' in kwargs or 'page' in request.GET or (hasattr(self, 'per_page') and self.per_page):
- page_num = kwargs.get('page', request.GET.get('page', 1))
- paginator, paginated_page, objects = paginate(objects, self.per_page, page_num)
- context.update({'paginator': paginator, 'paginated_page': paginated_page, self.list_var: objects})
- else:
- context.update({self.list_var: objects})
-
- return page.render_to_response(request, extra_context=context)
-
- return inner
-
- def feed_view(self, func, reverse_name):
- """
- Wraps an object-fetching function and renders the results as a rss or atom feed.
- """
- def inner(request, extra_context=None, **kwargs):
- objects, extra_context = func(request=request, extra_context=extra_context, **kwargs)
-
- if 'HTTP_ACCEPT' in request.META and 'rss' in request.META['HTTP_ACCEPT'] and 'atom' not in request.META['HTTP_ACCEPT']:
- feed_type = 'rss'
- else:
- feed_type = 'atom'
-
- current_site = Site.objects.get_current()
- #Could this be done with request.path instead somehow?
- feed_kwargs = {
- 'link': 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse(reverse_name, urlconf=self, kwargs=kwargs).strip('/'))
- }
- feed = self.get_feed(feed_type, extra_context, feed_kwargs)
-
- for obj in objects:
- kwargs = {
- 'link': 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), self.get_subpath(obj).strip('/'))
- }
- self.add_item(feed, obj, kwargs=kwargs)
-
- response = HttpResponse(mimetype=feed.mime_type)
- feed.write(response, 'utf-8')
- return response
-
- return inner
-
- def get_feed(self, feed_type, extra_context, kwargs=None):
- defaults = {
- 'description': ''
- }
- defaults.update(kwargs or {})
-
- if feed_type == 'rss':
- return self.rss_feed(**defaults)
-
- if 'description' in defaults and defaults['description'] and 'subtitle' not in defaults:
- defaults['subtitle'] = defaults['description']
-
- return self.atom_feed(**defaults)
-
- def feed_patterns(self, object_fetcher, page, base_name):
- urlpatterns = patterns('',
- url(r'^$', self.page_view(object_fetcher, page), name=base_name)
- )
- if self.feeds_enabled:
- feed_name = '%s_feed' % base_name
- urlpatterns = patterns('',
- url(r'^%s/$' % self.feed_suffix, self.feed_view(object_fetcher, feed_name), name=feed_name),
- ) + urlpatterns
- return urlpatterns
-
- def add_item(self, feed, obj, kwargs=None):
- defaults = kwargs or {}
- feed.add_item(**defaults)
\ No newline at end of file
model = NavigationItem
extra = 1
sortable_field_name = 'order'
+ related_lookup_fields = {'fk': raw_id_fields}
class NavigationItemChildInline(NavigationItemInline):
})
)
raw_id_fields = NAVIGATION_RAW_ID_FIELDS
+ related_lookup_fields = {'fk': raw_id_fields}
inlines = [NavigationItemChildInline] + TreeEntityAdmin.inlines
class NavigationAdmin(EntityAdmin):
inlines = [NavigationNavigationItemInline]
raw_id_fields = ['node']
+ related_lookup_fields = {'fk': raw_id_fields}
admin.site.unregister(Node)
--- /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 model 'Navigation'
+ db.create_table('shipherd_navigation', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('node', self.gf('django.db.models.fields.related.ForeignKey')(related_name='navigation_set', to=orm['philo.Node'])),
+ ('key', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('depth', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=3)),
+ ))
+ db.send_create_signal('shipherd', ['Navigation'])
+
+ # Adding unique constraint on 'Navigation', fields ['node', 'key']
+ db.create_unique('shipherd_navigation', ['node_id', 'key'])
+
+ # Adding model 'NavigationItem'
+ db.create_table('shipherd_navigationitem', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['shipherd.NavigationItem'])),
+ ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
+ ('lft', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)),
+ ('rght', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)),
+ ('tree_id', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)),
+ ('level', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)),
+ ('navigation', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='roots', null=True, to=orm['shipherd.Navigation'])),
+ ('text', self.gf('django.db.models.fields.CharField')(max_length=50)),
+ ('target_node', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='navigation_items', null=True, to=orm['philo.Node'])),
+ ('url_or_subpath', self.gf('django.db.models.fields.CharField')(max_length=200, blank=True)),
+ ('reversing_parameters', self.gf('philo.models.fields.JSONField')(blank=True)),
+ ('order', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=0)),
+ ))
+ db.send_create_signal('shipherd', ['NavigationItem'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'Navigation'
+ db.delete_table('shipherd_navigation')
+
+ # Removing unique constraint on 'Navigation', fields ['node', 'key']
+ db.delete_unique('shipherd_navigation', ['node_id', 'key'])
+
+ # Deleting model 'NavigationItem'
+ db.delete_table('shipherd_navigationitem')
+
+
+ models = {
+ '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'})
+ },
+ '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', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ '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', [], {'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', [], {})
+ },
+ 'shipherd.navigation': {
+ 'Meta': {'unique_together': "(('node', 'key'),)", 'object_name': 'Navigation'},
+ 'depth': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '3'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'navigation_set'", 'to': "orm['philo.Node']"})
+ },
+ 'shipherd.navigationitem': {
+ 'Meta': {'object_name': 'NavigationItem'},
+ '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'}),
+ 'navigation': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'roots'", 'null': 'True', 'to': "orm['shipherd.Navigation']"}),
+ 'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['shipherd.NavigationItem']"}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'navigation_items'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'text': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['shipherd']
--- /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 index on 'Navigation', fields ['key']
+ db.create_index('shipherd_navigation', ['key'])
+
+
+ def backwards(self, orm):
+
+ # Removing index on 'Navigation', fields ['key']
+ db.delete_index('shipherd_navigation', ['key'])
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", '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'})
+ },
+ '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', [], {})
+ },
+ 'shipherd.navigation': {
+ 'Meta': {'unique_together': "(('node', 'key'),)", 'object_name': 'Navigation'},
+ 'depth': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '3'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'navigation_set'", 'to': "orm['philo.Node']"})
+ },
+ 'shipherd.navigationitem': {
+ 'Meta': {'object_name': 'NavigationItem'},
+ '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'}),
+ 'navigation': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'roots'", 'null': 'True', 'to': "orm['shipherd.Navigation']"}),
+ 'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['shipherd.NavigationItem']"}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shipherd_navigationitem_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'text': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['shipherd']
from django.core.validators import RegexValidator, MinValueValidator
from django.db import models
from django.forms.models import model_to_dict
-from philo.models import TreeEntity, JSONField, Node, TreeManager, Entity
+from philo.models import TreeEntity, Node, TreeManager, Entity, TargetURLModel
from philo.validators import RedirectValidator
from UserDict import DictMixin
# 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(navigation_items__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)
objects = NavigationManager()
node = models.ForeignKey(Node, related_name='navigation_set', help_text="Be available as navigation for this node.")
- key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.")
+ key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.")
def __init__(self, *args, **kwargs):
return NavigationCacheQuerySet(self.model, using=self._db)
-class NavigationItem(TreeEntity):
+class NavigationItem(TreeEntity, TargetURLModel):
objects = NavigationItemManager()
navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.")
text = models.CharField(max_length=50)
- target_node = models.ForeignKey(Node, blank=True, null=True, related_name='navigation_items', help_text="Point to this node's url.")
- url_or_subpath = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.")
- 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.")
-
order = models.PositiveSmallIntegerField(default=0)
def __init__(self, *args, **kwargs):
return self.get_path(field='text', pathsep=u' › ')
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.")
-
- if self.reversing_parameters and (not self.url_or_subpath or not self.target_node):
- raise ValidationError("Reversing parameters require a view name and a target node.")
-
- try:
- self.get_target_url()
- except NoReverseMatch, e:
- raise ValidationError(e.message)
-
+ super(NavigationItem, self).clean()
if bool(self.parent) == bool(self.navigation):
raise ValidationError("Exactly one of `parent` and `navigation` must be defined.")
- def get_target_url(self):
- node = self.target_node
- if node is not None and node.accepts_subpath and self.url_or_subpath:
- if self.reversing_parameters is not None:
- view_name = self.url_or_subpath
- params = self.reversing_parameters
- args = isinstance(params, list) and params or None
- kwargs = isinstance(params, dict) and params or None
- return node.view.reverse(view_name, args=args, kwargs=kwargs, node=node)
- else:
- subpath = self.url_or_subpath
- while subpath and subpath[0] == '/':
- subpath = subpath[1:]
- return '%s%s' % (node.get_absolute_url(), subpath)
- elif node is not None:
- return node.get_absolute_url()
- else:
- return self.url_or_subpath
- target_url = property(get_target_url)
-
def is_active(self, request):
if self.target_url == request.path:
# Handle the `default` case where the target_url and requested path
@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('',
- 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')
+ return patterns('',
+ url(r'^login$', self.login, name='login'),
+ url(r'^logout$', self.logout, name='logout'),
)
-
- 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):
- current_site = Site.objects.get_current()
- token = token_generator.make_token(user, *(token_args or []))
- kwargs = {
- 'uidb36': int_to_base36(user.id),
- 'token': token
- }
- kwargs.update(reverse_kwargs or {})
- return 'http://%s%s' % (current_site.domain, self.reverse(confirmation_view, kwargs=kwargs, node=node))
-
- 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())
-
- context = self.get_context()
- context.update(extra_context or {})
+ self.set_requirement_redirect(request)
- 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
class ViewDoesNotProvideSubpaths(Exception):
- """ Raised by get_subpath when the View does not provide subpaths (the default). """
+ """ Raised by View.reverse when the View does not provide subpaths (the default). """
silent_variable_failure = True
class ViewCanNotProvideSubpath(Exception):
- """ Raised by get_subpath when the View can not provide a subpath for the supplied object. """
+ """ Raised by View.reverse when the View can not provide a subpath for the supplied arguments. """
silent_variable_failure = True
except Site.DoesNotExist:
current_site = None
+ path = request._cached_node_path
+ trailing_slash = False
+ if path[-1] == '/':
+ trailing_slash = True
+
try:
- node, subpath = Node.objects.get_with_path(request._cached_node_path, root=getattr(current_site, 'root_node', None), absolute_result=False)
+ node, subpath = Node.objects.get_with_path(path, root=getattr(current_site, 'root_node', None), absolute_result=False)
except Node.DoesNotExist:
node = None
if node:
+ if subpath is None:
+ subpath = ""
+ subpath = "/" + subpath
+
+ if trailing_slash and subpath[-1] != "/":
+ subpath += "/"
+
node.subpath = subpath
request._found_node = node
--- /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 'Redirect.target_node'
+ db.add_column('philo_redirect', 'target_node', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='philo_redirect_related', null=True, to=orm['philo.Node']), keep_default=False)
+
+ # Adding field 'Redirect.url_or_subpath'
+ db.add_column('philo_redirect', 'url_or_subpath', self.gf('django.db.models.fields.CharField')(default='', max_length=200, blank=True), keep_default=False)
+
+ # Adding field 'Redirect.reversing_parameters'
+ db.add_column('philo_redirect', 'reversing_parameters', self.gf('philo.models.fields.JSONField')(default='null', blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Redirect.target_node'
+ db.delete_column('philo_redirect', 'target_node_id')
+
+ # Deleting field 'Redirect.url_or_subpath'
+ db.delete_column('philo_redirect', 'url_or_subpath')
+
+ # Deleting field 'Redirect.reversing_parameters'
+ db.delete_column('philo_redirect', 'reversing_parameters_json')
+
+
+ models = {
+ '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'})
+ },
+ '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', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ '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', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ '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': '255'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'"})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", '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.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ '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 = ['philo']
--- /dev/null
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ for redirect in orm.Redirect.objects.all():
+ redirect.url_or_subpath = redirect.target
+ redirect.save()
+
+
+ def backwards(self, orm):
+ "This will cause data loss and is not advisable. Blurg!"
+ for redirect in orm.Redirect.objects.all():
+ redirect.target = redirect.url_or_subpath
+ redirect.save()
+
+
+ models = {
+ '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'})
+ },
+ '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', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ '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', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ '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': '255'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'"})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", '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.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ '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 = ['philo']
--- /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):
+
+ # Deleting field 'Redirect.target'
+ db.delete_column('philo_redirect', 'target')
+
+
+ def backwards(self, orm):
+
+ # Adding field 'Redirect.target'
+ db.add_column('philo_redirect', 'target', self.gf('django.db.models.fields.CharField')(default='', max_length=200), keep_default=False)
+
+
+ models = {
+ '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'})
+ },
+ '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', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ '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', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ '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': '255'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'"})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", '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.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ '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 = ['philo']
--- /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 index on 'Attribute', fields ['entity_object_id']
+ db.create_index('philo_attribute', ['entity_object_id'])
+
+ # Adding index on 'Attribute', fields ['value_object_id']
+ db.create_index('philo_attribute', ['value_object_id'])
+
+ # Adding index on 'Attribute', fields ['key']
+ db.create_index('philo_attribute', ['key'])
+
+
+ def backwards(self, orm):
+
+ # Removing index on 'Attribute', fields ['entity_object_id']
+ db.delete_index('philo_attribute', ['entity_object_id'])
+
+ # Removing index on 'Attribute', fields ['value_object_id']
+ db.delete_index('philo_attribute', ['value_object_id'])
+
+ # Removing index on 'Attribute', fields ['key']
+ db.delete_index('philo_attribute', ['key'])
+
+
+ models = {
+ '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'})
+ },
+ '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.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ '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': '255'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'"})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", '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.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ '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 = ['philo']
--- /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 index on 'Contentlet', fields ['name']
+ db.create_index('philo_contentlet', ['name'])
+
+ # Adding index on 'JSONValue', fields ['value']
+ db.create_index('philo_jsonvalue', ['value_json'])
+
+ # Adding index on 'ForeignKeyValue', fields ['object_id']
+ db.create_index('philo_foreignkeyvalue', ['object_id'])
+
+ # Adding index on 'ContentReference', fields ['name']
+ db.create_index('philo_contentreference', ['name'])
+
+
+ def backwards(self, orm):
+
+ # Removing index on 'ContentReference', fields ['name']
+ db.delete_index('philo_contentreference', ['name'])
+
+ # Removing index on 'ForeignKeyValue', fields ['object_id']
+ db.delete_index('philo_foreignkeyvalue', ['object_id'])
+
+ # Removing index on 'JSONValue', fields ['value']
+ db.delete_index('philo_jsonvalue', ['value_json'])
+
+ # Removing index on 'Contentlet', fields ['name']
+ db.delete_index('philo_contentlet', ['name'])
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", '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'})
+ },
+ '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.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ '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': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", '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.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'philo.tag': {
+ 'Meta': {'ordering': "('name',)", '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 = ['philo']
value_content_type_limiter.register_class(model)
+register_value_model(Tag)
+
+
def unregister_value_model(model):
value_content_type_limiter.unregister_class(model)
class JSONValue(AttributeValue):
- value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null')
+ 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)
class ForeignKeyValue(AttributeValue):
content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
- object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
+ object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
value = generic.GenericForeignKey()
def value_formfields(self):
class Attribute(models.Model):
entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
- entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
+ entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
value_content_type = models.ForeignKey(ContentType, related_name='attribute_value_set', limit_choices_to=attribute_value_limiter, verbose_name='Value type', null=True, blank=True)
- value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
+ value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
value = generic.GenericForeignKey('value_content_type', 'value_object_id')
- key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.")
+ key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
def __unicode__(self):
return u'"%s": %s' % (self.key, self.value)
# tree structure won't be that deep.
segments = path.split(pathsep)
- # Check for a trailing pathsep so we can restore it later.
- trailing_pathsep = False
- if segments[-1] == '':
- trailing_pathsep = True
-
# Clean out blank segments. Handles multiple consecutive pathseps.
while True:
try:
return kwargs
- def build_path(segments):
- path = pathsep.join(segments)
- if trailing_pathsep and segments and segments[-1] != '':
- path += pathsep
- return path
-
def find_obj(segments, depth, deepest_found=None):
if deepest_found is None:
deepest_level = 0
if deepest_level == depth:
# This should happen if nothing is found with any part of the given path.
if root is not None and deepest_found is None:
- return root, build_path(segments)
+ return root, pathsep.join(segments)
raise
return find_obj(segments, depth, deepest_found)
# Could there be a deeper one?
if obj.is_leaf_node():
- return obj, build_path(segments[deepest_level:]) or None
+ return obj, pathsep.join(segments[deepest_level:]) or None
depth += (len(segments) - depth)/2 or len(segments) - depth
depth = deepest_level + obj.get_descendant_count()
if deepest_level == depth:
- return obj, build_path(segments[deepest_level:]) or None
+ return obj, pathsep.join(segments[deepest_level:]) or None
try:
return find_obj(segments, depth, obj)
except self.model.DoesNotExist:
# Then this was the deepest.
- return obj, build_path(segments[deepest_level:])
+ return obj, pathsep.join(segments[deepest_level:])
if absolute_result:
return self.get(**make_query_kwargs(segments, root))
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
-from django.contrib.sites.models import Site
-from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect
-from django.core.exceptions import ViewDoesNotExist
+from django.contrib.sites.models import Site, RequestSite
+from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect, Http404
+from django.core.exceptions import ValidationError
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 inspect import getargspec
from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
+from philo.models.fields import JSONField
from philo.utils import ContentTypeSubclassLimiter
from philo.validators import RedirectValidator
from philo.exceptions import ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths, AncestorDoesNotExist
return self.view.accepts_subpath
return False
+ def handles_subpath(self, subpath):
+ return self.view.handles_subpath(subpath)
+
def render_to_response(self, request, extra_context=None):
return self.view.render_to_response(request, extra_context)
- def get_absolute_url(self):
+ def get_absolute_url(self, request=None, with_domain=False, secure=False):
+ return self.construct_url(request=request, with_domain=with_domain, secure=secure)
+
+ def construct_url(self, subpath="/", request=None, with_domain=False, secure=False):
+ """
+ This method will construct a URL based on the Node's location.
+ If a request is passed in, that will be used as a backup in case
+ the Site lookup fails. The Site lookup takes precedence because
+ it's what's used to find the root node. This will raise:
+ - NoReverseMatch if philo-root is not reverseable
+ - Site.DoesNotExist if a domain is requested but not buildable.
+ - AncestorDoesNotExist if the root node of the site isn't an
+ ancestor of this instance.
+ """
+ # Try reversing philo-root first, since we can't do anything if that fails.
+ root_url = reverse('philo-root')
+
try:
- root = Site.objects.get_current().root_node
+ current_site = Site.objects.get_current()
except Site.DoesNotExist:
- root = None
+ if request is not None:
+ current_site = RequestSite(request)
+ elif with_domain:
+ # If they want a domain and we can't figure one out,
+ # best to reraise the error to let them know.
+ raise
+ else:
+ current_site = None
- try:
- path = self.get_path(root=root)
- if path:
- path += '/'
- root_url = reverse('philo-root')
- return '%s%s' % (root_url, path)
- except AncestorDoesNotExist, ViewDoesNotExist:
- return None
+ root = getattr(current_site, 'root_node', None)
+ path = self.get_path(root=root)
+
+ if current_site and with_domain:
+ domain = "http%s://%s" % (secure and "s" or "", current_site.domain)
+ else:
+ domain = ""
+
+ if not path or subpath == "/":
+ subpath = subpath[1:]
+
+ return '%s%s%s%s' % (domain, root_url, path, subpath)
class Meta:
app_label = 'philo'
accepts_subpath = False
- def get_subpath(self, obj):
+ def handles_subpath(self, subpath):
+ if not self.accepts_subpath and subpath != "/":
+ return False
+ return True
+
+ def reverse(self, view_name=None, args=None, kwargs=None, node=None, obj=None):
+ """Shortcut method to handle the common pattern of getting the
+ absolute url for a view's subpaths."""
if not self.accepts_subpath:
raise ViewDoesNotProvideSubpaths
- view_name, args, kwargs = self.get_reverse_params(obj)
+ if obj is not None:
+ # Perhaps just override instead of combining?
+ obj_view_name, obj_args, obj_kwargs = self.get_reverse_params(obj)
+ if view_name is None:
+ view_name = obj_view_name
+ args = list(obj_args) + list(args or [])
+ obj_kwargs.update(kwargs or {})
+ kwargs = obj_kwargs
+
try:
- return reverse(view_name, args=args, kwargs=kwargs, urlconf=self)
- except NoReverseMatch:
- raise ViewCanNotProvideSubpath
+ subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {})
+ except NoReverseMatch, e:
+ raise ViewCanNotProvideSubpath(e.message)
+
+ if node is not None:
+ return node.construct_url(subpath)
+ return subpath
def get_reverse_params(self, obj):
"""This method should return a view_name, args, kwargs tuple suitable for reversing a url for the given obj using self as the urlconf."""
return response
def actually_render_to_response(self, request, extra_context=None):
- raise NotImplementedError('View subclasses must implement render_to_response.')
+ raise NotImplementedError('View subclasses must implement actually_render_to_response.')
class Meta:
abstract = True
accepts_subpath = True
@property
- def urlpatterns(self, obj):
+ def urlpatterns(self):
raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
+ def handles_subpath(self, subpath):
+ if not super(MultiView, self).handles_subpath(subpath):
+ return False
+ try:
+ resolve(subpath, urlconf=self)
+ except Http404:
+ return False
+ return True
+
def actually_render_to_response(self, request, extra_context=None):
clear_url_caches()
subpath = request.node.subpath
- if not subpath:
- subpath = ""
- subpath = "/" + subpath
view, args, kwargs = resolve(subpath, urlconf=self)
view_args = getargspec(view)
if extra_context is not None and ('extra_context' in view_args[0] or view_args[2] is not None):
kwargs['extra_context'] = extra_context
return view(request, *args, **kwargs)
- def reverse(self, view_name, args=None, kwargs=None, node=None):
- """Shortcut method to handle the common pattern of getting the absolute url for a multiview's
- subpaths."""
- subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {})
- if node is not None:
- return '/%s/%s/' % (node.get_absolute_url().strip('/'), subpath.strip('/'))
- return subpath
-
def get_context(self):
"""Hook for providing instance-specific context - such as the value of a Field - to all views."""
return {}
- def basic_view(self, view_name):
+ def basic_view(self, field_name):
"""
- Wraps a field name and returns a simple view function that will render that view
- with a basic context. This assumes that the field name is a ForeignKey to a
- model with a render_to_response method.
+ Given the name of a field on ``self``, accesses the value of
+ that field and treats it as a ``View`` instance. Creates a
+ basic context based on self.get_context() and any extra_context
+ that was passed in, then calls the ``View`` instance's
+ render_to_response() method. This method is meant to be called
+ to return a view function appropriate for urlpatterns.
"""
- field = self._meta.get_field(view_name)
+ field = self._meta.get_field(field_name)
view = getattr(self, field.name, None)
def inner(request, extra_context=None, **kwargs):
abstract = True
-class Redirect(View):
+class TargetURLModel(models.Model):
+ target_node = models.ForeignKey(Node, blank=True, null=True, related_name="%(app_label)s_%(class)s_related")
+ url_or_subpath = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.")
+ 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.")
+
+ if self.reversing_parameters and not (self.url_or_subpath or self.target_node):
+ raise ValidationError("Reversing parameters require either a view name or a target node.")
+
+ try:
+ self.get_target_url()
+ except (NoReverseMatch, ViewCanNotProvideSubpath), e:
+ raise ValidationError(e.message)
+
+ super(TargetURLModel, self).clean()
+
+ def get_reverse_params(self):
+ params = self.reversing_parameters
+ 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 = {}
+ for key, val in params.items():
+ if isinstance(key, unicode):
+ key = str(key)
+ kwargs[key] = val
+ return self.url_or_subpath, args, kwargs
+
+ def get_target_url(self):
+ node = self.target_node
+ if node is not None and node.accepts_subpath and self.url_or_subpath:
+ if self.reversing_parameters is not None:
+ view_name, args, kwargs = self.get_reverse_params()
+ subpath = node.view.reverse(view_name, args=args, kwargs=kwargs)
+ else:
+ subpath = self.url_or_subpath
+ if subpath[0] != '/':
+ subpath = '/' + subpath
+ return node.construct_url(subpath)
+ elif node is not None:
+ return node.get_absolute_url()
+ else:
+ if self.reversing_parameters is not None:
+ view_name, args, kwargs = self.get_reverse_params()
+ return reverse(view_name, args=args, kwargs=kwargs)
+ return self.url_or_subpath
+ target_url = property(get_target_url)
+
+ class Meta:
+ abstract = True
+
+
+class Redirect(View, TargetURLModel):
STATUS_CODES = (
(302, 'Temporary'),
(301, 'Permanent'),
)
- target = models.CharField(max_length=200, validators=[RedirectValidator()])
status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
def actually_render_to_response(self, request, extra_context=None):
- response = HttpResponseRedirect(self.target)
+ response = HttpResponseRedirect(self.target_url)
response.status_code = self.status_code
return response
app_label = 'philo'
-# Why does this exist?
class File(View):
""" For storing arbitrary files """
class Contentlet(models.Model):
page = models.ForeignKey(Page, related_name='contentlets')
- name = models.CharField(max_length=255)
+ name = models.CharField(max_length=255, db_index=True)
content = TemplateField()
def __unicode__(self):
class ContentReference(models.Model):
page = models.ForeignKey(Page, related_name='contentreferences')
- name = models.CharField(max_length=255)
+ name = models.CharField(max_length=255, db_index=True)
content_type = models.ForeignKey(ContentType, verbose_name='Content type')
content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
content = generic.GenericForeignKey('content_type', 'content_id')
raise
return settings.TEMPLATE_STRING_IF_INVALID
else:
- if subpath[0] == '/':
- subpath = subpath[1:]
-
- url = node.get_absolute_url() + subpath
+ url = node.construct_url(subpath)
if self.as_var:
context[self.as_var] = url
from django.conf import settings
-from django.http import Http404
+from django.core.urlresolvers import resolve
+from django.http import Http404, HttpResponseRedirect
from django.views.decorators.vary import vary_on_headers
from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
raise MIDDLEWARE_NOT_CONFIGURED
if not request.node:
+ if settings.APPEND_SLASH and request.path != "/":
+ path = request.path
+
+ if path[-1] == "/":
+ path = path[:-1]
+ else:
+ path += "/"
+
+ view, args, kwargs = resolve(path)
+ if view != node_view:
+ return HttpResponseRedirect(path)
raise Http404
node = request.node
subpath = request.node.subpath
- if subpath and not node.accepts_subpath:
- raise Http404
+ # Explicitly disallow trailing slashes if we are otherwise at a node's url.
+ if request.path and request.path != "/" and request.path[-1] == "/" and subpath == "/":
+ return HttpResponseRedirect(node.get_absolute_url())
+
+ if not node.handles_subpath(subpath):
+ # If the subpath isn't handled, check settings.APPEND_SLASH. If
+ # it's True, try to correct the subpath.
+ if not settings.APPEND_SLASH:
+ raise Http404
+
+ if subpath[-1] == "/":
+ subpath = subpath[:-1]
+ else:
+ subpath += "/"
+
+ redirect_url = node.construct_url(subpath)
+
+ if node.handles_subpath(subpath):
+ return HttpResponseRedirect(redirect_url)
+
+ # Perhaps there is a non-philo view at this address. Can we
+ # resolve *something* there besides node_view? If not,
+ # raise a 404.
+ view, args, kwargs = resolve(redirect_url)
+
+ if view == node_view:
+ raise Http404
+ else:
+ return HttpResponseRedirect(redirect_url)
+
return node.render_to_response(request, kwargs)
\ No newline at end of file