mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-22 14:01:53 +00:00
Move 'core' app to the wagtail package
This commit is contained in:
parent
679f15811e
commit
5c69d3ae23
28 changed files with 11242 additions and 0 deletions
0
wagtail/wagtailcore/__init__.py
Normal file
0
wagtail/wagtailcore/__init__.py
Normal file
22
wagtail/wagtailcore/admin.py
Normal file
22
wagtail/wagtailcore/admin.py
Normal 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)
|
||||
32
wagtail/wagtailcore/fields.py
Normal file
32
wagtail/wagtailcore/fields.py
Normal 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"])
|
||||
0
wagtail/wagtailcore/management/__init__.py
Normal file
0
wagtail/wagtailcore/management/__init__.py
Normal file
0
wagtail/wagtailcore/management/commands/__init__.py
Normal file
0
wagtail/wagtailcore/management/commands/__init__.py
Normal file
46
wagtail/wagtailcore/management/commands/fixtree.py
Normal file
46
wagtail/wagtailcore/management/commands/fixtree.py
Normal 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."
|
||||
21
wagtail/wagtailcore/management/commands/move_pages.py
Normal file
21
wagtail/wagtailcore/management/commands/move_pages.py
Normal 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'
|
||||
37
wagtail/wagtailcore/management/commands/replace_text.py
Normal file
37
wagtail/wagtailcore/management/commands/replace_text.py
Normal 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)
|
||||
14
wagtail/wagtailcore/management/commands/set_url_paths.py
Normal file
14
wagtail/wagtailcore/management/commands/set_url_paths.py
Normal 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, '/')
|
||||
12
wagtail/wagtailcore/middleware.py
Normal file
12
wagtail/wagtailcore/middleware.py
Normal 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
|
||||
155
wagtail/wagtailcore/migrations/0001_initial.py
Normal file
155
wagtail/wagtailcore/migrations/0001_initial.py
Normal 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']
|
||||
138
wagtail/wagtailcore/migrations/0002_initial_data.py
Normal file
138
wagtail/wagtailcore/migrations/0002_initial_data.py
Normal 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
|
||||
701
wagtail/wagtailcore/models.py
Normal file
701
wagtail/wagtailcore/models.py
Normal 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
|
||||
220
wagtail/wagtailcore/rich_text.py
Normal file
220
wagtail/wagtailcore/rich_text.py
Normal 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
|
||||
1
wagtail/wagtailcore/static/css/.gitkeep
Normal file
1
wagtail/wagtailcore/static/css/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
Dummy file so that this directory can be tracked in the vagrant-django-template git repository even when empty
|
||||
1
wagtail/wagtailcore/static/images/.gitkeep
Normal file
1
wagtail/wagtailcore/static/images/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
Dummy file so that this directory can be tracked in the vagrant-django-template git repository even when empty
|
||||
9597
wagtail/wagtailcore/static/js/jquery-1.9.1.js
vendored
Normal file
9597
wagtail/wagtailcore/static/js/jquery-1.9.1.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
wagtail/wagtailcore/static/lib/.gitkeep
Normal file
1
wagtail/wagtailcore/static/lib/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
Dummy file so that this directory can be tracked in the vagrant-django-template git repository even when empty
|
||||
51
wagtail/wagtailcore/templates/base.html
Normal file
51
wagtail/wagtailcore/templates/base.html
Normal 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>
|
||||
9
wagtail/wagtailcore/templates/core/page.html
Normal file
9
wagtail/wagtailcore/templates/core/page.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ self.title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ self.title }}</h1>
|
||||
</body>
|
||||
</html>
|
||||
0
wagtail/wagtailcore/templatetags/__init__.py
Normal file
0
wagtail/wagtailcore/templatetags/__init__.py
Normal file
11
wagtail/wagtailcore/templatetags/pageurl.py
Normal file
11
wagtail/wagtailcore/templatetags/pageurl.py
Normal 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)
|
||||
10
wagtail/wagtailcore/templatetags/rich_text.py
Normal file
10
wagtail/wagtailcore/templatetags/rich_text.py
Normal 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))
|
||||
16
wagtail/wagtailcore/tests.py
Normal file
16
wagtail/wagtailcore/tests.py
Normal 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)
|
||||
10
wagtail/wagtailcore/urls.py
Normal file
10
wagtail/wagtailcore/urls.py
Normal 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' )
|
||||
)
|
||||
5
wagtail/wagtailcore/util.py
Normal file
5
wagtail/wagtailcore/util.py
Normal 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('_')
|
||||
11
wagtail/wagtailcore/views.py
Normal file
11
wagtail/wagtailcore/views.py
Normal 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)
|
||||
121
wagtail/wagtailcore/whitelist.py
Normal file
121
wagtail/wagtailcore/whitelist.py
Normal 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()
|
||||
Loading…
Reference in a new issue