Move 'core' app to the wagtail package

This commit is contained in:
Matt Westcott 2014-01-24 11:42:11 +00:00
parent 679f15811e
commit 5c69d3ae23
28 changed files with 11242 additions and 0 deletions

View file

View file

@ -0,0 +1,22 @@
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import GroupAdmin
from wagtail.wagtailcore.models import Site, Page, GroupPagePermission
admin.site.register(Site)
admin.site.register(Page)
# Extend GroupAdmin to include page permissions as an inline
class GroupPagePermissionInline(admin.TabularInline):
model = GroupPagePermission
raw_id_fields = ['page']
verbose_name = 'page permission'
verbose_name_plural = 'page permissions'
class GroupAdminWithPagePermissions(GroupAdmin):
inlines = GroupAdmin.inlines + [GroupPagePermissionInline]
admin.site.unregister(Group)
admin.site.register(Group, GroupAdminWithPagePermissions)

View file

@ -0,0 +1,32 @@
from django.db import models
from django.forms import Textarea
from south.modelsinspector import add_introspection_rules
from wagtail.wagtailcore.rich_text import DbWhitelister, expand_db_html
class RichTextArea(Textarea):
def get_panel(self):
from verdantadmin.edit_handlers import RichTextFieldPanel
return RichTextFieldPanel
def render(self, name, value, attrs=None):
if value is None:
translated_value = None
else:
translated_value = expand_db_html(value, for_editor=True)
return super(RichTextArea, self).render(name, translated_value, attrs)
def value_from_datadict(self, data, files, name):
original_value = super(RichTextArea, self).value_from_datadict(data, files, name)
if original_value is None:
return None
return DbWhitelister.clean(original_value)
class RichTextField(models.TextField):
def formfield(self, **kwargs):
defaults = {'widget': RichTextArea}
defaults.update(kwargs)
return super(RichTextField, self).formfield(**defaults)
add_introspection_rules([], ["^wagtail\.wagtailcore\.fields\.RichTextField"])

View file

@ -0,0 +1,46 @@
from django.core.management.base import NoArgsCommand
from django.core.exceptions import ObjectDoesNotExist
from wagtail.wagtailcore.models import Page
class Command(NoArgsCommand):
def handle_noargs(self, **options):
problems_found = False
for page in Page.objects.all():
try:
page.specific
except ObjectDoesNotExist:
print "Page %d (%s) is missing a subclass record; deleting." % (page.id, page.title)
problems_found = True
page.delete()
(_, _, _, bad_depth, bad_numchild) = Page.find_problems()
if bad_depth:
print "Incorrect depth value found for pages: %r" % bad_depth
if bad_numchild:
print "Incorrect numchild value found for pages: %r" % bad_numchild
if bad_depth or bad_numchild:
Page.fix_tree(destructive=False)
problems_found = True
remaining_problems = Page.find_problems()
if any(remaining_problems):
print "Remaining problems (cannot fix automatically):"
(bad_alpha, bad_path, orphans, bad_depth, bad_numchild) = remaining_problems
if bad_alpha:
print "Invalid characters found in path for pages: %r" % bad_alpha
if bad_path:
print "Invalid path length found for pages: %r" % bad_path
if orphans:
print "Orphaned pages found: %r" % orphans
if bad_depth:
print "Incorrect depth value found for pages: %r" % bad_depth
if bad_numchild:
print "Incorrect numchild value found for pages: %r" % bad_numchild
elif problems_found:
print "All problems fixed."
else:
print "No problems found."

View file

@ -0,0 +1,21 @@
from django.core.management.base import BaseCommand
from wagtail.wagtailcore.models import Page
class Command(BaseCommand):
def handle(self, _from_id, _to_id, **options):
# Convert args to integers
from_id = int(_from_id)
to_id = int(_to_id)
# Get pages
from_page = Page.objects.get(pk=from_id)
to_page = Page.objects.get(pk=to_id)
pages = from_page.get_children()
# Move the pages
print 'Moving ' + str(len(pages)) + ' pages from "' + from_page.title + '" to "' + to_page.title + '"'
for page in pages:
page.move(to_page, pos='last-child')
print 'Done'

View file

@ -0,0 +1,37 @@
from django.core.management.base import BaseCommand
from django.db import models
from wagtail.wagtailcore.models import PageRevision, get_page_types
def replace_in_model(model, from_text, to_text):
text_field_names = [field.name for field in model._meta.fields if isinstance(field, models.TextField) or isinstance(field, models.CharField)]
updated_fields = []
for field in text_field_names:
field_value = getattr(model, field)
if field_value and (from_text in field_value):
updated_fields.append(field)
setattr(model, field, field_value.replace(from_text, to_text))
if updated_fields:
model.save(update_fields=updated_fields)
class Command(BaseCommand):
def handle(self, from_text, to_text, **options):
for revision in PageRevision.objects.filter(content_json__contains=from_text):
revision.content_json = revision.content_json.replace(from_text, to_text)
revision.save(update_fields=['content_json'])
for content_type in get_page_types():
print "scanning %s" % content_type.name
page_class = content_type.model_class()
try:
child_relation_names = [rel.get_accessor_name() for rel in page_class._meta.child_relations]
except AttributeError:
child_relation_names = []
for page in page_class.objects.all():
replace_in_model(page, from_text, to_text)
for child_rel in child_relation_names:
for child in getattr(page, child_rel).all():
replace_in_model(child, from_text, to_text)

View file

@ -0,0 +1,14 @@
from django.core.management.base import NoArgsCommand
from wagtail.wagtailcore.models import Page
class Command(NoArgsCommand):
def set_subtree(self, root, root_path):
root.url_path = root_path
root.save(update_fields=['url_path'])
for child in root.get_children():
self.set_subtree(child, root_path + child.slug + '/')
def handle_noargs(self, **options):
for node in Page.get_root_nodes():
self.set_subtree(node, '/')

View file

@ -0,0 +1,12 @@
from wagtail.wagtailcore.models import Site
class SiteMiddleware(object):
def process_request(self, request):
"""
Set request.site to contain the Site object responsible for handling this request,
according to hostname matching rules
"""
try:
request.site = Site.find_for_request(request)
except Site.DoesNotExist:
request.site = None

View file

@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as 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 'Site'
db.create_table(u'wagtailcore_site', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('hostname', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255, db_index=True)),
('port', self.gf('django.db.models.fields.IntegerField')(default=80)),
('root_page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='sites_rooted_here', to=orm['wagtailcore.Page'])),
('is_default_site', self.gf('django.db.models.fields.BooleanField')(default=False)),
))
db.send_create_signal(u'wagtailcore', ['Site'])
# Adding model 'Page'
db.create_table(u'wagtailcore_page', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('path', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
('depth', self.gf('django.db.models.fields.PositiveIntegerField')()),
('numchild', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
('slug', self.gf('django.db.models.fields.SlugField')(max_length=50)),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='pages', to=orm['contenttypes.ContentType'])),
('live', self.gf('django.db.models.fields.BooleanField')(default=True)),
('has_unpublished_changes', self.gf('django.db.models.fields.BooleanField')(default=False)),
('url_path', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
('owner', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='owned_pages', null=True, to=orm['auth.User'])),
('seo_title', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
('show_in_menus', self.gf('django.db.models.fields.BooleanField')(default=False)),
('search_description', self.gf('django.db.models.fields.TextField')(blank=True)),
))
db.send_create_signal(u'wagtailcore', ['Page'])
# Adding model 'PageRevision'
db.create_table(u'wagtailcore_pagerevision', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='revisions', to=orm['wagtailcore.Page'])),
('submitted_for_moderation', self.gf('django.db.models.fields.BooleanField')(default=False)),
('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
('content_json', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal(u'wagtailcore', ['PageRevision'])
# Adding model 'GroupPagePermission'
db.create_table(u'wagtailcore_grouppagepermission', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('group', self.gf('django.db.models.fields.related.ForeignKey')(related_name='page_permissions', to=orm['auth.Group'])),
('page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='group_permissions', to=orm['wagtailcore.Page'])),
('permission_type', self.gf('django.db.models.fields.CharField')(max_length=20)),
))
db.send_create_signal(u'wagtailcore', ['GroupPagePermission'])
def backwards(self, orm):
# Deleting model 'Site'
db.delete_table(u'wagtailcore_site')
# Deleting model 'Page'
db.delete_table(u'wagtailcore_page')
# Deleting model 'PageRevision'
db.delete_table(u'wagtailcore_pagerevision')
# Deleting model 'GroupPagePermission'
db.delete_table(u'wagtailcore_grouppagepermission')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'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'}),
u'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'})
},
u'wagtailcore.grouppagepermission': {
'Meta': {'object_name': 'GroupPagePermission'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': u"orm['wagtailcore.Page']"}),
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
},
u'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['auth.User']"}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
},
u'wagtailcore.pagerevision': {
'Meta': {'object_name': 'PageRevision'},
'content_json': ('django.db.models.fields.TextField', [], {}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': u"orm['wagtailcore.Page']"}),
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'wagtailcore.site': {
'Meta': {'object_name': 'Site'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': u"orm['wagtailcore.Page']"})
}
}
complete_apps = ['wagtailcore']

View file

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
class Migration(DataMigration):
def forwards(self, orm):
page_content_type, created = orm['contenttypes.contenttype'].objects.get_or_create(
model='page', app_label='core', defaults={'name': 'page'})
root = orm['wagtailcore.page'].objects.create(
title="Root",
slug='root',
content_type=page_content_type,
path='0001',
depth=1,
numchild=1,
url_path='/',
)
homepage = orm['wagtailcore.page'].objects.create(
title="Welcome to your new Verdant site!",
slug='home',
content_type=page_content_type,
path='00010001',
depth=2,
numchild=0,
url_path='/home/',
)
orm['wagtailcore.site'].objects.create(
hostname='localhost', root_page=homepage, is_default_site=True)
editors_group = orm['auth.group'].objects.create(name='Editors')
moderators_group = orm['auth.group'].objects.create(name='Moderators')
orm['wagtailcore.grouppagepermission'].objects.create(
group=moderators_group, page=root, permission_type='add')
orm['wagtailcore.grouppagepermission'].objects.create(
group=moderators_group, page=root, permission_type='edit')
orm['wagtailcore.grouppagepermission'].objects.create(
group=moderators_group, page=root, permission_type='publish')
orm['wagtailcore.grouppagepermission'].objects.create(
group=editors_group, page=root, permission_type='add')
orm['wagtailcore.grouppagepermission'].objects.create(
group=editors_group, page=root, permission_type='edit')
def backwards(self, orm):
orm['auth.group'].objects.filter(name__in=('Editors', 'Moderators')).delete()
orm['wagtailcore.site'].objects.all().delete()
orm['wagtailcore.page'].objects.all().delete()
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'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'}),
u'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'})
},
u'wagtailcore.grouppagepermission': {
'Meta': {'object_name': 'GroupPagePermission'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': u"orm['wagtailcore.Page']"}),
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
},
u'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['auth.User']"}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
},
u'wagtailcore.pagerevision': {
'Meta': {'object_name': 'PageRevision'},
'content_json': ('django.db.models.fields.TextField', [], {}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': u"orm['wagtailcore.Page']"}),
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'wagtailcore.site': {
'Meta': {'object_name': 'Site'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': u"orm['wagtailcore.Page']"})
}
}
complete_apps = ['wagtailcore']
symmetrical = True

View file

@ -0,0 +1,701 @@
from django.db import models, connection, transaction
from django.db.models import get_model, Q
from django.http import Http404
from django.shortcuts import render
from django.core.cache import cache
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Group
from treebeard.mp_tree import MP_Node
from cluster.models import ClusterableModel
from verdantsearch import Indexed, Searcher
from wagtail.wagtailcore.util import camelcase_to_underscore
class SiteManager(models.Manager):
def get_by_natural_key(self, hostname):
return self.get(hostname=hostname)
class Site(models.Model):
hostname = models.CharField(max_length=255, unique=True, db_index=True)
port = models.IntegerField(default=80, help_text="Set this to something other than 80 if you need a specific port number to appear in URLs (e.g. development on port 8000). Does not affect request handling (so port forwarding still works).")
root_page = models.ForeignKey('Page', related_name='sites_rooted_here')
is_default_site = models.BooleanField(default=False, help_text="If true, this site will handle requests for all other hostnames that do not have a site entry of their own")
def natural_key(self):
return (self.hostname,)
def __unicode__(self):
return self.hostname + ("" if self.port == 80 else (":%d" % self.port)) + (" [default]" if self.is_default_site else "")
@staticmethod
def find_for_request(request):
"""Find the site object responsible for responding to this HTTP request object"""
hostname = request.META['HTTP_HOST'].split(':')[0]
try:
# find a Site matching this specific hostname
return Site.objects.get(hostname=hostname)
except Site.DoesNotExist:
# failing that, look for a catch-all Site. If that fails, let the Site.DoesNotExist propagate back to the caller
return Site.objects.get(is_default_site=True)
@property
def root_url(self):
if self.port == 80:
return 'http://%s' % self.hostname
elif self.port == 443:
return 'https://%s' % self.hostname
else:
return 'http://%s:%d' % (self.hostname, self.port)
# clear the verdant_site_root_paths cache whenever Site records are updated
def save(self, *args, **kwargs):
result = super(Site, self).save(*args, **kwargs)
cache.delete('verdant_site_root_paths')
return result
@staticmethod
def get_site_root_paths():
"""
Return a list of (root_path, root_url) tuples, most specific path first -
used to translate url_paths into actual URLs with hostnames
"""
result = cache.get('verdant_site_root_paths')
if result is None:
result = [
(site.id, site.root_page.url_path, site.root_url)
for site in Site.objects.select_related('root_page').order_by('-root_page__url_path')
]
cache.set('verdant_site_root_paths', result, 3600)
return result
PAGE_MODEL_CLASSES = []
_PAGE_CONTENT_TYPES = []
def get_page_types():
global _PAGE_CONTENT_TYPES
if len(_PAGE_CONTENT_TYPES) != len(PAGE_MODEL_CLASSES):
_PAGE_CONTENT_TYPES = [
ContentType.objects.get_for_model(cls) for cls in PAGE_MODEL_CLASSES
]
return _PAGE_CONTENT_TYPES
LEAF_PAGE_MODEL_CLASSES = []
_LEAF_PAGE_CONTENT_TYPE_IDS = []
def get_leaf_page_content_type_ids():
global _LEAF_PAGE_CONTENT_TYPE_IDS
if len(_LEAF_PAGE_CONTENT_TYPE_IDS) != len(LEAF_PAGE_MODEL_CLASSES):
_LEAF_PAGE_CONTENT_TYPE_IDS = [
ContentType.objects.get_for_model(cls).id for cls in LEAF_PAGE_MODEL_CLASSES
]
return _LEAF_PAGE_CONTENT_TYPE_IDS
NAVIGABLE_PAGE_MODEL_CLASSES = []
_NAVIGABLE_PAGE_CONTENT_TYPE_IDS = []
def get_navigable_page_content_type_ids():
global _NAVIGABLE_PAGE_CONTENT_TYPE_IDS
if len(_NAVIGABLE_PAGE_CONTENT_TYPE_IDS) != len(NAVIGABLE_PAGE_MODEL_CLASSES):
_NAVIGABLE_PAGE_CONTENT_TYPE_IDS = [
ContentType.objects.get_for_model(cls).id for cls in NAVIGABLE_PAGE_MODEL_CLASSES
]
return _NAVIGABLE_PAGE_CONTENT_TYPE_IDS
class PageBase(models.base.ModelBase):
"""Metaclass for Page"""
def __init__(cls, name, bases, dct):
super(PageBase, cls).__init__(name, bases, dct)
if cls._deferred:
# this is an internal class built for Django's deferred-attribute mechanism;
# don't proceed with all this page type registration stuff
return
if 'template' not in dct:
# Define a default template path derived from the app name and model name
cls.template = "%s/%s.html" % (cls._meta.app_label, camelcase_to_underscore(name))
cls._clean_subpage_types = None # to be filled in on first call to cls.clean_subpage_types
if not dct.get('is_abstract'):
# subclasses are only abstract if the subclass itself defines itself so
cls.is_abstract = False
if not cls.is_abstract:
# register this type in the list of page content types
PAGE_MODEL_CLASSES.append(cls)
if cls.subpage_types:
NAVIGABLE_PAGE_MODEL_CLASSES.append(cls)
else:
LEAF_PAGE_MODEL_CLASSES.append(cls)
class Page(MP_Node, ClusterableModel, Indexed):
__metaclass__ = PageBase
title = models.CharField(max_length=255, help_text="The page title as you'd like it to be seen by the public")
slug = models.SlugField()
# TODO: enforce uniqueness on slug field per parent (will have to be done at the Django
# level rather than db, since there is no explicit parent relation in the db)
content_type = models.ForeignKey('contenttypes.ContentType', related_name='pages')
live = models.BooleanField(default=True, editable=False)
has_unpublished_changes = models.BooleanField(default=False, editable=False)
url_path = models.CharField(max_length=255, blank=True, editable=False)
owner = models.ForeignKey('auth.User', null=True, blank=True, editable=False, related_name='owned_pages')
seo_title = models.CharField("Page title", max_length=255, blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.")
show_in_menus = models.BooleanField(default=False, help_text="Whether a link to this page will appear in automatically generated menus")
search_description = models.TextField(blank=True)
indexed_fields = {
'title': {
'type': 'string',
'analyzer': 'edgengram_analyzer',
'boost': 100,
},
'live': {
'type': 'boolean',
'analyzer': 'simple',
},
}
search_backend = Searcher(None)
search_frontend = Searcher(None, filters=dict(live=True))
title_search_backend = Searcher(['title'])
title_search_frontend = Searcher(['title'], filters=dict(live=True))
search_name = None
def __init__(self, *args, **kwargs):
super(Page, self).__init__(*args, **kwargs)
if not self.id and not self.content_type_id:
# this model is being newly created rather than retrieved from the db;
# set content type to correctly represent the model class that this was
# created as
self.content_type = ContentType.objects.get_for_model(self)
def __unicode__(self):
return self.title
# by default pages do not allow any kind of subpages
subpage_types = []
is_abstract = True # don't offer Page in the list of page types a superuser can create
def set_url_path(self, parent):
"""
Populate the url_path field based on this page's slug and the specified parent page.
(We pass a parent in here, rather than retrieving it via get_parent, so that we can give
new unsaved pages a meaningful URL when previewing them; at that point the page has not
been assigned a position in the tree, as far as treebeard is concerned.
"""
if parent:
self.url_path = parent.url_path + self.slug + '/'
else:
# a page without a parent is the tree root, which always has a url_path of '/'
self.url_path = '/'
return self.url_path
@transaction.commit_on_success # ensure that changes are only committed when we have updated all descendant URL paths, to preserve consistency
def save(self, *args, **kwargs):
update_descendant_url_paths = False
if self.id is None:
# we are creating a record. If we're doing things properly, this should happen
# through a treebeard method like add_child, in which case the 'path' field
# has been set and so we can safely call get_parent
self.set_url_path(self.get_parent())
else:
# see if the slug has changed from the record in the db, in which case we need to
# update url_path of self and all descendants
old_record = Page.objects.get(id=self.id)
if old_record.slug != self.slug:
self.set_url_path(self.get_parent())
update_descendant_url_paths = True
old_url_path = old_record.url_path
new_url_path = self.url_path
result = super(Page, self).save(*args, **kwargs)
if update_descendant_url_paths:
self._update_descendant_url_paths(old_url_path, new_url_path)
return result
def _update_descendant_url_paths(self, old_url_path, new_url_path):
cursor = connection.cursor()
cursor.execute("""
UPDATE wagtailcore_page
SET url_path = %s || substring(url_path from %s)
WHERE path LIKE %s AND id <> %s
""", [new_url_path, len(old_url_path) + 1, self.path + '%', self.id])
def object_indexed(self):
# Exclude root node from index
if self.depth == 1:
return False
return True
@property
def specific(self):
"""
Return this page in its most specific subclassed form.
"""
# the ContentType.objects manager keeps a cache, so this should potentially
# avoid a database lookup over doing self.content_type. I think.
content_type = ContentType.objects.get_for_id(self.content_type_id)
if isinstance(self, content_type.model_class()):
# self is already the an instance of the most specific class
return self
else:
return content_type.get_object_for_this_type(id=self.id)
@property
def specific_class(self):
"""
return the class that this page would be if instantiated in its
most specific form
"""
content_type = ContentType.objects.get_for_id(self.content_type_id)
return content_type.model_class()
def route(self, request, path_components):
if path_components:
# request is for a child of this page
child_slug = path_components[0]
remaining_components = path_components[1:]
try:
subpage = self.get_children().get(slug=child_slug)
except Page.DoesNotExist:
raise Http404
return subpage.specific.route(request, remaining_components)
else:
# request is for this very page
if self.live:
return self.serve(request)
else:
raise Http404
def save_revision(self, user=None, submitted_for_moderation=False):
self.revisions.create(content_json=self.to_json(), user=user, submitted_for_moderation=submitted_for_moderation)
def get_latest_revision(self):
try:
revision = self.revisions.order_by('-created_at')[0]
except IndexError:
return False
return revision
def get_latest_revision_as_page(self):
try:
revision = self.revisions.order_by('-created_at')[0]
except IndexError:
return self.specific
return revision.as_page_object()
def serve(self, request):
return render(request, self.template, {
'self': self
})
def is_navigable(self):
"""
Return true if it's meaningful to browse subpages of this page -
i.e. it currently has subpages, or its page type indicates that sub-pages are supported,
or it's at the top level (this rule necessary for empty out-of-the-box sites to have working navigation)
"""
return (not self.is_leaf()) or (self.content_type_id not in get_leaf_page_content_type_ids()) or self.depth == 2
def get_other_siblings(self):
# get sibling pages excluding self
return self.get_siblings().exclude(id=self.id)
@property
def url(self):
for (id, root_path, root_url) in Site.get_site_root_paths():
if self.url_path.startswith(root_path):
return root_url + self.url_path[len(root_path) - 1:]
def relative_url(self, current_site):
for (id, root_path, root_url) in Site.get_site_root_paths():
if self.url_path.startswith(root_path):
return ('' if current_site.id == id else root_url) + self.url_path[len(root_path) - 1:]
@classmethod
def clean_subpage_types(cls):
"""
Returns the list of subpage types, with strings converted to class objects
where required
"""
if cls._clean_subpage_types is None:
res = []
for page_type in cls.subpage_types:
if isinstance(page_type, basestring):
try:
app_label, model_name = page_type.split(".")
except ValueError:
# If we can't split, assume a model in current app
app_label = cls._meta.app_label
model_name = page_type
model = get_model(app_label, model_name)
if model:
res.append(model)
else:
raise NameError("name '%s' (used in subpage_types list) is not defined " % page_type)
else:
# assume it's already a model class
res.append(page_type)
cls._clean_subpage_types = res
return cls._clean_subpage_types
@classmethod
def allowed_parent_page_types(cls):
"""
Returns the list of page types that this page type can be a subpage of
"""
return [ct for ct in get_page_types() if cls in ct.model_class().clean_subpage_types()]
@classmethod
def allowed_parent_pages(cls):
"""
Returns the list of pages that this page type can be a subpage of
"""
return Page.objects.filter(content_type__in=cls.allowed_parent_page_types())
@classmethod
def get_verbose_name(cls):
# This is similar to doing cls._meta.verbose_name.title()
# except this doesn't convert any characters to lowercase
return ' '.join([word[0].upper() + word[1:] for word in cls._meta.verbose_name.split()])
@property
def status_string(self):
if not self.live:
return "draft"
else:
if self.has_unpublished_changes:
return "live + draft"
else:
return "live"
def has_unpublished_subtree(self):
"""
An awkwardly-defined flag used in determining whether unprivileged editors have
permission to delete this article. Returns true if and only if this page is non-live,
and it has no live children.
"""
return (not self.live) and (not self.get_descendants().filter(live=True).exists())
@transaction.commit_on_success # only commit when all descendants are properly updated
def move(self, target, pos=None):
"""
Extension to the treebeard 'move' method to ensure that url_path is updated too.
"""
old_url_path = Page.objects.get(id=self.id).url_path
super(Page, self).move(target, pos=pos)
# treebeard's move method doesn't actually update the in-memory instance, so we need to work
# with a freshly loaded one now
new_self = Page.objects.get(id=self.id)
new_url_path = new_self.set_url_path(new_self.get_parent())
new_self.save()
new_self._update_descendant_url_paths(old_url_path, new_url_path)
def permissions_for_user(self, user):
"""
Return a PagePermissionsTester object defining what actions the user can perform on this page
"""
user_perms = UserPagePermissionsProxy(user)
return user_perms.for_page(self)
def get_navigation_menu_items():
# Get all pages that appear in the navigation menu: ones which have children,
# or are a non-leaf type (indicating that they *could* have children),
# or are at the top-level (this rule required so that an empty site out-of-the-box has a working menu)
navigable_content_type_ids = get_navigable_page_content_type_ids()
if navigable_content_type_ids:
pages = Page.objects.raw("""
SELECT * FROM wagtailcore_page
WHERE numchild > 0 OR content_type_id IN %s OR depth = 2
ORDER BY path
""", [tuple(navigable_content_type_ids)])
else:
pages = Page.objects.raw("""
SELECT * FROM wagtailcore_page
WHERE numchild > 0 OR depth = 2
ORDER BY path
""")
# Turn this into a tree structure:
# tree_node = (page, children)
# where 'children' is a list of tree_nodes.
# Algorithm:
# Maintain a list that tells us, for each depth level, the last page we saw at that depth level.
# Since our page list is ordered by path, we know that whenever we see a page
# at depth d, its parent must be the last page we saw at depth (d-1), and so we can
# find it in that list.
depth_list = [(None, [])] # a dummy node for depth=0, since one doesn't exist in the DB
for page in pages:
# create a node for this page
node = (page, [])
# retrieve the parent from depth_list
parent_page, parent_childlist = depth_list[page.depth - 1]
# insert this new node in the parent's child list
parent_childlist.append(node)
# add the new node to depth_list
try:
depth_list[page.depth] = node
except IndexError:
# an exception here means that this node is one level deeper than any we've seen so far
depth_list.append(node)
# in Verdant, the convention is to have one root node in the db (depth=1); the menu proper
# begins with the children of that node (depth=2).
try:
root, root_children = depth_list[1]
return root_children
except IndexError:
# what, we don't even have a root node? Fine, just return an empty list...
[]
class Orderable(models.Model):
sort_order = models.IntegerField(null=True, blank=True, editable=False)
sort_order_field = 'sort_order'
class Meta:
abstract = True
ordering = ['sort_order']
class SubmittedRevisionsManager(models.Manager):
def get_query_set(self):
return super(SubmittedRevisionsManager, self).get_query_set().filter(submitted_for_moderation=True)
class PageRevision(models.Model):
page = models.ForeignKey('Page', related_name='revisions')
submitted_for_moderation = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey('auth.User', null=True, blank=True)
content_json = models.TextField()
objects = models.Manager()
submitted_revisions = SubmittedRevisionsManager()
def save(self, *args, **kwargs):
super(PageRevision, self).save(*args, **kwargs)
if self.submitted_for_moderation:
# ensure that all other revisions of this page have the 'submitted for moderation' flag unset
self.page.revisions.exclude(id=self.id).update(submitted_for_moderation=False)
def as_page_object(self):
obj = self.page.specific_class.from_json(self.content_json)
# Override the possibly-outdated tree parameter fields from this revision object
# with up-to-date values
obj.path = self.page.path
obj.depth = self.page.depth
obj.numchild = self.page.numchild
# Populate url_path based on the revision's current slug and the parent page as determined
# by path
obj.set_url_path(self.page.get_parent())
# also copy over other properties which are meaningful for the page as a whole, not a
# specific revision of it
obj.live = self.page.live
obj.has_unpublished_changes = self.page.has_unpublished_changes
obj.owner = self.page.owner
return obj
def publish(self):
page = self.as_page_object()
page.live = True
page.save()
self.submitted_for_moderation = False
page.revisions.update(submitted_for_moderation=False)
PAGE_PERMISSION_TYPE_CHOICES = [
('add', 'Add'),
('edit', 'Edit'),
('publish', 'Publish'),
]
class GroupPagePermission(models.Model):
group = models.ForeignKey(Group, related_name='page_permissions')
page = models.ForeignKey('Page', related_name='group_permissions')
permission_type = models.CharField(max_length=20, choices=PAGE_PERMISSION_TYPE_CHOICES)
class UserPagePermissionsProxy(object):
"""Helper object that encapsulates all the page permission rules that this user has
across the page hierarchy."""
def __init__(self, user):
self.user = user
if user.is_active and not user.is_superuser:
self.permissions = GroupPagePermission.objects.filter(group__user=self.user).select_related('page')
def revisions_for_moderation(self):
"""Return a queryset of page revisions awaiting moderation that this user has publish permission on"""
# Deal with the trivial cases first...
if not self.user.is_active:
return PageRevision.objects.none()
if self.user.is_superuser:
return PageRevision.submitted_revisions.all()
# get the list of pages for which they have direct publish permission (i.e. they can publish any page within this subtree)
publishable_pages = [perm.page for perm in self.permissions if perm.permission_type == 'publish']
if not publishable_pages:
return PageRevision.objects.none()
# compile a filter expression to apply to the PageRevision.submitted_revisions manager:
# return only those pages whose paths start with one of the publishable_pages paths
only_my_sections = Q(page__path__startswith=publishable_pages[0].path)
for page in publishable_pages[1:]:
only_my_sections = only_my_sections | Q(page__path__startswith=page.path)
# return the filtered queryset
return PageRevision.submitted_revisions.filter(only_my_sections)
def for_page(self, page):
"""Return a PagePermissionTester object that can be used to query whether this user has
permission to perform specific tasks on the given page"""
return PagePermissionTester(self, page)
class PagePermissionTester(object):
def __init__(self, user_perms, page):
self.user = user_perms.user
self.user_perms = user_perms
self.page = page
if self.user.is_active and not self.user.is_superuser:
self.permissions = set(
perm.permission_type for perm in user_perms.permissions
if self.page.path.startswith(perm.page.path)
)
def can_add_subpage(self):
if not self.user.is_active:
return False
return self.user.is_superuser or ('add' in self.permissions)
def can_edit(self):
if not self.user.is_active:
return False
if self.page.is_root(): # root node is not a page and can never be edited, even by superusers
return False
return self.user.is_superuser or ('edit' in self.permissions) or ('add' in self.permissions and self.page.owner_id == self.user.id)
def can_delete(self):
if not self.user.is_active:
return False
if self.page.is_root(): # root node is not a page and can never be deleted, even by superusers
return False
if self.user.is_superuser or ('publish' in self.permissions):
# Users with publish permission can unpublish any pages that need to be unpublished to achieve deletion
return True
elif 'edit' in self.permissions:
# user can only delete if there are no live pages in this subtree
return (not self.page.live) and (not self.page.get_descendants().filter(live=True).exists())
elif 'add' in self.permissions:
# user can only delete if all pages in this subtree are unpublished and owned by this user
return (
(not self.page.live)
and (self.page.owner_id == self.user.id)
and (not self.page.get_descendants().exclude(live=False, owner=self.user).exists())
)
else:
return False
def can_unpublish(self):
if not self.user.is_active:
return False
if (not self.page.live) or self.page.is_root():
return False
return self.user.is_superuser or ('publish' in self.permissions)
def can_publish(self):
if not self.user.is_active:
return False
if self.page.is_root():
return False
return self.user.is_superuser or ('publish' in self.permissions)
def can_publish_subpage(self):
"""
Niggly special case for creating and publishing a page in one go.
Differs from can_publish in that we want to be able to publish subpages of root, but not
to be able to publish root itself
"""
if not self.user.is_active:
return False
return self.user.is_superuser or ('publish' in self.permissions)
def can_reorder_children(self):
"""
Keep reorder permissions the same as publishing, since it immediately affects published pages
(and the use-cases for a non-admin needing to do it are fairly obscure...)
"""
return self.can_publish_subpage()
def can_move(self):
"""
Moving a page should be logically equivalent to deleting and re-adding it (and all its children).
As such, the permission test for 'can this be moved at all?' should be the same as for deletion.
(Further constraints will then apply on where it can be moved *to*.)
"""
return self.can_delete()
def can_move_to(self, destination):
# reject the logically impossible cases first
if self.page == destination or destination.is_child_of(self.page):
return False
# and shortcut the trivial 'everything' / 'nothing' permissions
if not self.user.is_active:
return False
if self.user.is_superuser:
return True
# check that the page can be moved at all
if not self.can_move():
return False
# Inspect permissions on the destination
destination_perms = self.user_perms.for_page(destination)
# we always need at least add permission in the target
if 'add' not in destination_perms.permissions:
return False
if self.page.live or self.page.get_descendants().filter(live=True).exists():
# moving this page will entail publishing within the destination section
return ('publish' in destination_perms.permissions)
else:
# no publishing required, so the already-tested 'add' permission is sufficient
return True

View file

@ -0,0 +1,220 @@
from django.utils.html import escape
from bs4 import BeautifulSoup
import re # parsing HTML with regexes LIKE A BOSS.
from wagtail.wagtailcore.whitelist import Whitelister
from wagtail.wagtailcore.models import Page
# FIXME: we don't really want to import verdantimages within core.
# For that matter, we probably don't want core to be concerned about translating
# HTML for the benefit of the hallo.js editor...
from verdantimages.models import get_image_model
from verdantimages.formats import get_image_format
from verdantdocs.models import Document
# Define a set of 'embed handlers' and 'link handlers'. These handle the translation
# of 'special' HTML elements in rich text - ones which we do not want to include
# verbatim in the DB representation because they embed information which is stored
# elsewhere in the database and is liable to change - from real HTML representation
# to DB representation and back again.
class ImageEmbedHandler(object):
"""
ImageEmbedHandler will be invoked whenever we encounter an element in HTML content
with an attribute of data-embedtype="image". The resulting element in the database
representation will be:
<embed embedtype="image" id="42" format="thumb" alt="some custom alt text">
"""
@staticmethod
def get_db_attributes(tag):
"""
Given a tag that we've identified as an image embed (because it has a
data-embedtype="image" attribute), return a dict of the attributes we should
have on the resulting <embed> element.
"""
return {
'id': tag['data-id'],
'format': tag['data-format'],
'alt': tag['data-alt'],
}
@staticmethod
def expand_db_attributes(attrs, for_editor):
"""
Given a dict of attributes from the <embed> tag, return the real HTML
representation.
"""
Image = get_image_model()
try:
image = Image.objects.get(id=attrs['id'])
format = get_image_format(attrs['format'])
if for_editor:
return format.image_to_editor_html(image, attrs['alt'])
else:
return format.image_to_html(image, attrs['alt'])
except Image.DoesNotExist:
return "<img>"
class MediaEmbedHandler(object):
"""
MediaEmbedHandler will be invoked whenever we encounter an element in HTML content
with an attribute of data-embedtype="media". The resulting element in the database
representation will be:
<embed embedtype="media" url="http://vimeo.com/XXXXX">
"""
@staticmethod
def get_db_attributes(tag):
"""
Given a tag that we've identified as a media embed (because it has a
data-embedtype="media" attribute), return a dict of the attributes we should
have on the resulting <embed> element.
"""
return {
'url': tag['data-url'],
}
@staticmethod
def expand_db_attributes(attrs, for_editor):
"""
Given a dict of attributes from the <embed> tag, return the real HTML
representation.
"""
from verdantembeds import format
if for_editor:
return format.embed_to_editor_html(attrs['url'])
else:
return format.embed_to_frontend_html(attrs['url'])
class PageLinkHandler(object):
"""
PageLinkHandler will be invoked whenever we encounter an <a> element in HTML content
with an attribute of data-linktype="page". The resulting element in the database
representation will be:
<a linktype="page" id="42">hello world</a>
"""
@staticmethod
def get_db_attributes(tag):
"""
Given an <a> tag that we've identified as a page link embed (because it has a
data-linktype="page" attribute), return a dict of the attributes we should
have on the resulting <a linktype="page"> element.
"""
return {'id': tag['data-id']}
@staticmethod
def expand_db_attributes(attrs, for_editor):
try:
page = Page.objects.get(id=attrs['id'])
if for_editor:
editor_attrs = 'data-linktype="page" data-id="%d" ' % page.id
else:
editor_attrs = ''
return '<a %shref="%s">' % (editor_attrs, escape(page.url))
except Page.DoesNotExist:
return "<a>"
class DocumentLinkHandler(object):
@staticmethod
def get_db_attributes(tag):
return {'id': tag['data-id']}
@staticmethod
def expand_db_attributes(attrs, for_editor):
try:
doc = Document.objects.get(id=attrs['id'])
if for_editor:
editor_attrs = 'data-linktype="document" data-id="%d" ' % doc.id
else:
editor_attrs = ''
return '<a %shref="%s">' % (editor_attrs, escape(doc.url))
except Document.DoesNotExist:
return "<a>"
EMBED_HANDLERS = {
'image': ImageEmbedHandler,
'media': MediaEmbedHandler,
}
LINK_HANDLERS = {
'page': PageLinkHandler,
'document': DocumentLinkHandler,
}
# Prepare a whitelisting engine with custom behaviour:
# rewrite any elements with a data-embedtype or data-linktype attribute
class DbWhitelister(Whitelister):
@classmethod
def clean_tag_node(cls, doc, tag):
if 'data-embedtype' in tag.attrs:
embed_type = tag['data-embedtype']
# fetch the appropriate embed handler for this embedtype
embed_handler = EMBED_HANDLERS[embed_type]
embed_attrs = embed_handler.get_db_attributes(tag)
embed_attrs['embedtype'] = embed_type
embed_tag = doc.new_tag('embed', **embed_attrs)
embed_tag.can_be_empty_element = True
tag.replace_with(embed_tag)
elif tag.name == 'a' and 'data-linktype' in tag.attrs:
# first, whitelist the contents of this tag
for child in tag.contents:
cls.clean_node(doc, child)
link_type = tag['data-linktype']
link_handler = LINK_HANDLERS[link_type]
link_attrs = link_handler.get_db_attributes(tag)
link_attrs['linktype'] = link_type
tag.attrs.clear()
tag.attrs.update(**link_attrs)
elif tag.name == 'div':
tag.name = 'p'
else:
super(DbWhitelister, cls).clean_tag_node(doc, tag)
FIND_A_TAG = re.compile(r'<a(\b[^>]*)>')
FIND_EMBED_TAG = re.compile(r'<embed(\b[^>]*)/>')
FIND_ATTRS = re.compile(r'([\w-]+)\="([^"]*)"')
def extract_attrs(attr_string):
"""
helper method to extract tag attributes as a dict. Does not escape HTML entities!
"""
attributes = {}
for name, val in FIND_ATTRS.findall(attr_string):
attributes[name] = val
return attributes
def expand_db_html(html, for_editor=False):
"""
Expand database-representation HTML into proper HTML usable in either
templates or the rich text editor
"""
def replace_a_tag(m):
attrs = extract_attrs(m.group(1))
if 'linktype' not in attrs:
# return unchanged
return m.group(0)
handler = LINK_HANDLERS[attrs['linktype']]
return handler.expand_db_attributes(attrs, for_editor)
def replace_embed_tag(m):
attrs = extract_attrs(m.group(1))
handler = EMBED_HANDLERS[attrs['embedtype']]
return handler.expand_db_attributes(attrs, for_editor)
html = FIND_A_TAG.sub(replace_a_tag, html)
html = FIND_EMBED_TAG.sub(replace_embed_tag, html)
return html

View file

@ -0,0 +1 @@
Dummy file so that this directory can be tracked in the vagrant-django-template git repository even when empty

View file

@ -0,0 +1 @@
Dummy file so that this directory can be tracked in the vagrant-django-template git repository even when empty

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
Dummy file so that this directory can be tracked in the vagrant-django-template git repository even when empty

View file

@ -0,0 +1,51 @@
<!doctype html>
{% load compress %}
{% comment %} paulirish.com/2008/conditional-stylesheets-vs-css-hacks-answer-neither/ {% endcomment %}
<!--[if lt IE 7]> <html class="lt-ie10 lt-ie9 lt-ie8 lt-ie7" lang="en"> <![endif]-->
<!--[if IE 7]> <html class="lt-ie10 lt-ie9 lt-ie8" lang="en"> <![endif]-->
<!--[if IE 8]> <html class="lt-ie10 lt-ie9" lang="en"> <![endif]-->
<!--[if IE 9]> <html class="lt-ie10" lang="en"> <![endif]-->
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title></title>
{% compress css %}
{% comment %} Core CSS includes (for inclusion on all pages) {% endcomment %}
{% endcompress %}
{% compress css %}
{% comment %}
Per-page CSS is compressed separately so that the global stuff doesn't get re-downloaded each time
{% endcomment %}
{% block extra_css %}{% endblock %}
{% endcompress %}
</head>
<body>
{% block content %}{% endblock %}
{% comment %}
JavaScript at the bottom for fast page loading:
http://developer.yahoo.com/performance/rules.html#js_bottom
{% endcomment %}
{% compress js %}
{% comment %}
Core JS includes (for inclusion on all pages) to be specified here
{% endcomment %}
<script src="{{ STATIC_URL }}js/jquery-1.9.1.js"></script>
{% endcompress %}
{% compress js %}
{% comment %}
Per-page JS is compressed separately so that the global stuff doesn't get re-downloaded each time
{% endcomment %}
{% block extra_js %}{% endblock %}
{% endcompress %}
</body>
</html>

View file

@ -0,0 +1,9 @@
<!DOCTYPE HTML>
<html>
<head>
<title>{{ self.title }}</title>
</head>
<body>
<h1>{{ self.title }}</h1>
</body>
</html>

View file

@ -0,0 +1,11 @@
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def pageurl(context, page):
"""
Outputs a page's URL as relative (/foo/bar/) if it's within the same site as the
current page, or absolute (http://example.com/foo/bar/) if not.
"""
return page.relative_url(context['request'].site)

View file

@ -0,0 +1,10 @@
from django import template
from django.utils.safestring import mark_safe
from wagtail.wagtailcore.rich_text import expand_db_html
register = template.Library()
@register.filter
def richtext(value):
return mark_safe(expand_db_html(value))

View file

@ -0,0 +1,16 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

View file

@ -0,0 +1,10 @@
from django.conf.urls import patterns, url
urlpatterns = patterns('wagtail.wagtailcore.views',
# All front-end views are handled through Wagtail's core.views.serve mechanism.
# Here we match a (possibly empty) list of path segments, each followed by
# a '/'. If a trailing slash is not present, we leave CommonMiddleware to
# handle it as usual (i.e. redirect it to the trailing slash version if
# settings.APPEND_SLASH is True)
url(r'^((?:[\w\-]+/)*)$', 'serve' )
)

View file

@ -0,0 +1,5 @@
import re
def camelcase_to_underscore(str):
# http://djangosnippets.org/snippets/585/
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', str).lower().strip('_')

View file

@ -0,0 +1,11 @@
from django.http import Http404
def serve(request, path):
# we need a valid Site object corresponding to this request (set in wagtail.wagtailcore.middleware.SiteMiddleware)
# in order to proceed
if not request.site:
raise Http404
path_components = [component for component in path.split('/') if component]
return request.site.root_page.specific.route(request, path_components)

View file

@ -0,0 +1,121 @@
"""
A generic HTML whitelisting engine, designed to accommodate subclassing to override
specific rules.
"""
from bs4 import BeautifulSoup, NavigableString, Tag
from urlparse import urlparse
ALLOWED_URL_SCHEMES = ['', 'http', 'https', 'ftp', 'mailto', 'tel']
def check_url(url_string):
# TODO: more paranoid checks (urlparse doesn't catch "jav\tascript:alert('XSS')")
url = urlparse(url_string)
return (url_string if url.scheme in ALLOWED_URL_SCHEMES else None)
def attribute_rule(allowed_attrs):
"""
Generator for functions that can be used as entries in Whitelister.element_rules.
These functions accept a tag, and modify its attributes by looking each attribute
up in the 'allowed_attrs' dict defined here:
* if the lookup fails, drop the attribute
* if the lookup returns a callable, replace the attribute with the result of calling
it - e.g. {'title': uppercase} will replace 'title' with the result of uppercasing
the title. If the callable returns None, the attribute is dropped
* if the lookup returns a truthy value, keep the attribute; if falsy, drop it
"""
def fn(tag):
for attr, val in tag.attrs.items():
rule = allowed_attrs.get(attr)
if rule:
if callable(rule):
new_val = rule(val)
if new_val is None:
del tag[attr]
else:
tag[attr] = new_val
else:
# rule is not callable, just truthy - keep the attribute
pass
else:
# rule is falsy or absent - remove the attribute
del tag[attr]
return fn
allow_without_attributes = attribute_rule({})
class Whitelister(object):
element_rules = {
'[document]': allow_without_attributes,
'a': attribute_rule({'href': check_url}),
'b': allow_without_attributes,
'br': allow_without_attributes,
'div': allow_without_attributes,
'em': allow_without_attributes,
'h1': allow_without_attributes,
'h2': allow_without_attributes,
'h3': allow_without_attributes,
'h4': allow_without_attributes,
'h5': allow_without_attributes,
'h6': allow_without_attributes,
'hr': allow_without_attributes,
'i': allow_without_attributes,
'img': attribute_rule({'src': check_url, 'width': True, 'height': True, 'alt': True}),
'li': allow_without_attributes,
'ol': allow_without_attributes,
'p': allow_without_attributes,
'strong': allow_without_attributes,
'sub': allow_without_attributes,
'sup': allow_without_attributes,
'ul': allow_without_attributes,
}
@classmethod
def clean(cls, html):
"""Clean up an HTML string to contain just the allowed elements / attributes"""
doc = BeautifulSoup(html, 'lxml')
cls.clean_node(doc, doc)
return unicode(doc)
@classmethod
def clean_node(cls, doc, node):
"""Clean a BeautifulSoup document in-place"""
if isinstance(node, NavigableString):
cls.clean_string_node(doc, node)
elif isinstance(node, Tag):
cls.clean_tag_node(doc, node)
else:
cls.clean_unknown_node(doc, node)
@classmethod
def clean_string_node(cls, doc, str):
# by default, nothing needs to be done to whitelist string nodes
pass
@classmethod
def clean_tag_node(cls, doc, tag):
# first, whitelist the contents of this tag
# NB tag.contents will change while this iteration is running, so we need
# to capture the initial state into a static list() and iterate over that
# to avoid losing our place in the sequence.
for child in list(tag.contents):
cls.clean_node(doc, child)
# see if there is a rule in element_rules for this tag type
try:
rule = cls.element_rules[tag.name]
except KeyError:
# don't recognise this tag name, so KILL IT WITH FIRE
tag.unwrap()
return
# apply the rule
rule(tag)
@classmethod
def clean_unknown_node(cls, doc, node):
# don't know what type of object this is, so KILL IT WITH FIRE
node.decompose()