Skip to content

Instantly share code, notes, and snippets.

@dsummersl
Last active September 10, 2018 15:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dsummersl/b20857017cc30343660e82b6f64f1243 to your computer and use it in GitHub Desktop.
Save dsummersl/b20857017cc30343660e82b6f64f1243 to your computer and use it in GitHub Desktop.
wagtail streamblocks
from django.utils.safestring import mark_safe
from wagtail.wagtailcore.blocks import (
StructBlock, StructValue, CharBlock, PageChooserBlock, URLBlock,
StreamBlock, StaticBlock)
from wagtail.wagtailimages.blocks import ImageChooserBlock
class PageLink(StructBlock):
"""Links to pages within this site.
"""
title = CharBlock(max_length=255)
page = PageChooserBlock()
class Meta:
icon = 'doc-full'
template = 'blocks/page_link.html'
class ExternalLink(StructBlock):
"""Links out to external sites, or that otherwise need a raw URL.
"""
title = CharBlock(max_length=255, label="Page title")
url = URLBlock()
class Meta:
icon = 'link'
template = 'blocks/external_link.html'
class AddressLink(StructBlock):
"""A link with the structure of a physical address.
"""
name = CharBlock(max_length=255)
street_number = CharBlock(max_length=255)
city_state_zip = CharBlock(max_length=255)
url = URLBlock()
class Meta:
icon = 'mail'
template = 'blocks/address_link.html'
class FooterCategoryMenuBlock(StructBlock):
"""A navigation submenu that can pull in upon request all of its child
pages that are marked as Show in Menu.
"""
menu_title = CharBlock(
max_length=20,
help_text='Name of category in footer')
menu_category = PageChooserBlock(target_model='home.CategoryPage')
menu_items = StaticBlock(
admin_text=mark_safe(
"<p>"
"Pages will automatically be displayed as menu items under the following conditions: "
"1) if they are published as immediate children of this category, and "
"2) if 'Show in menus' is checked in their edit mode."
"</p>"
)
)
class Meta:
icon = 'folder-open-inverse'
template = 'blocks/category_menu.html'
def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context=parent_context)
pl = PageLink()
if value['menu_category']:
context['menu_items'] = [
pl.bind(StructValue(self, [('title', item.title), ('page', item)]))
for item in value['menu_category'].get_children().live().public().in_menu()
]
return context
class HeaderCategoryMenuBlock(FooterCategoryMenuBlock):
class Meta:
icon = 'folder-open-inverse'
template = 'blocks/header_category_menu.html'
class FooterStaticMenuBlock(StructBlock):
"""A navigation submenu that just contains explicitly defined links.
"""
menu_title = CharBlock(max_length=255)
menu_items = StreamBlock((
('page', PageLink()),
('external_link', ExternalLink()),
('address', AddressLink()),
))
class Meta:
icon = 'folder-open-inverse'
template = 'blocks/static_menu.html'
def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context=parent_context)
context['menu_items'] = list(value['menu_items'])
return context
class HeaderStaticMenuBlock(StructBlock):
"""A static navigation submenu that just contains explicitly defined links
with an image and link.
To preserve the order that the fields in this StructBlock appear, this class
does not extend FooterStaticMenuBlock.
"""
menu_title = CharBlock(max_length=255)
menu_description = CharBlock(
max_length=255,
help_text='Text that appears in the mega-nav above list of pages in this category.')
menu_image = ImageChooserBlock(
help_text='Display a preview image and link to a page in this category.')
menu_image_text = CharBlock(
max_length=25,
help_text='Link text displayed below image')
menu_image_url = URLBlock(
help_text='Link displayed below image')
menu_items = StreamBlock((
('page', PageLink()),
('external_link', ExternalLink()),
('address', AddressLink()),
))
class Meta:
icon = 'folder-open-inverse'
template = 'blocks/header_static_menu.html'
def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context=parent_context)
context['menu_items'] = list(value['menu_items'])
return context
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-07-28 20:53
from __future__ import unicode_literals
from django.db import migrations
import wagtail.wagtailcore.blocks
import wagtail.wagtailcore.blocks.static_block
import wagtail.wagtailcore.fields
import wagtail.wagtailimages.blocks
from ..wagtail import AddField, RemoveField, migrate
v7_block_definition = wagtail.wagtailcore.blocks.StreamBlock((
('category_menu', wagtail.wagtailcore.blocks.StructBlock((
('menu_title', wagtail.wagtailcore.blocks.CharBlock()),
('menu_category', wagtail.wagtailcore.blocks.PageChooserBlock()),
('menu_items', wagtail.wagtailcore.blocks.StaticBlock()),
))),
('static_menu', wagtail.wagtailcore.blocks.StructBlock((
('menu_title', wagtail.wagtailcore.blocks.CharBlock()),
('menu_items', wagtail.wagtailcore.blocks.StreamBlock(((
('page', wagtail.wagtailcore.blocks.StructBlock((
('title', wagtail.wagtailcore.blocks.CharBlock()),
('page', wagtail.wagtailcore.blocks.PageChooserBlock()),
))),
('external_link', wagtail.wagtailcore.blocks.StructBlock((
('title', wagtail.wagtailcore.blocks.CharBlock()),
('url', wagtail.wagtailcore.blocks.URLBlock()),
))),
('address', wagtail.wagtailcore.blocks.StructBlock((
('name', wagtail.wagtailcore.blocks.CharBlock()),
('street_number', wagtail.wagtailcore.blocks.CharBlock()),
('city_state_zip', wagtail.wagtailcore.blocks.CharBlock()),
('url', wagtail.wagtailcore.blocks.URLBlock()),
))),
)))),
)))
))
v8_block_definition = wagtail.wagtailcore.blocks.StreamBlock((
('category_menu', wagtail.wagtailcore.blocks.StructBlock((
('menu_title', wagtail.wagtailcore.blocks.CharBlock()),
('menu_category', wagtail.wagtailcore.blocks.PageChooserBlock()),
('menu_items', wagtail.wagtailcore.blocks.StaticBlock()),
('menu_description', wagtail.wagtailcore.blocks.CharBlock()),
('menu_image', wagtail.wagtailcore.blocks.PageChooserBlock()),
))),
('static_menu', wagtail.wagtailcore.blocks.StructBlock((
('menu_title', wagtail.wagtailcore.blocks.CharBlock()),
('menu_description', wagtail.wagtailcore.blocks.CharBlock()),
('menu_image', wagtail.wagtailcore.blocks.PageChooserBlock()),
('menu_image_text', wagtail.wagtailcore.blocks.CharBlock()),
('menu_image_url', wagtail.wagtailcore.blocks.URLBlock()),
('menu_items', wagtail.wagtailcore.blocks.StreamBlock(((
('page', wagtail.wagtailcore.blocks.StructBlock((
('title', wagtail.wagtailcore.blocks.CharBlock()),
('page', wagtail.wagtailcore.blocks.PageChooserBlock()),
))),
('external_link', wagtail.wagtailcore.blocks.StructBlock((
('title', wagtail.wagtailcore.blocks.CharBlock()),
('url', wagtail.wagtailcore.blocks.URLBlock()),
))),
('address', wagtail.wagtailcore.blocks.StructBlock((
('name', wagtail.wagtailcore.blocks.CharBlock()),
('street_number', wagtail.wagtailcore.blocks.CharBlock()),
('city_state_zip', wagtail.wagtailcore.blocks.CharBlock()),
('url', wagtail.wagtailcore.blocks.URLBlock()),
))),
)))),
)))
))
def add_fields(apps, schema_editor):
HeaderMenu = apps.get_model('sitenav', 'headermenu')
migrate(HeaderMenu, 'menus', v7_block_definition, v8_block_definition, {
'category_menu': [
AddField('menu_description', 'Menu description'),
AddField('menu_image')
],
'static_menu': [
AddField('menu_description', 'Static menu description'),
AddField('menu_image'),
AddField('menu_image_text', 'Menu image text'),
AddField('menu_image_url', 'http://example.com')
]
})
def remove_fields(apps, schema_editor):
HeaderMenu = apps.get_model('sitenav', 'headermenu')
migrate(HeaderMenu, 'menus', v8_block_definition, v7_block_definition, {
'category_menu': [
RemoveField('menu_description'),
RemoveField('menu_image')
],
'static_menu': [
RemoveField('menu_description'),
RemoveField('menu_image'),
RemoveField('menu_image_text'),
RemoveField('menu_image_url')
]
})
class Migration(migrations.Migration):
dependencies = [
('sitenav', '0007_auto_20170707_1550'),
]
operations = [
migrations.AlterField(
model_name='footermenu',
name='menus',
field=wagtail.wagtailcore.fields.StreamField((('category_menu', wagtail.wagtailcore.blocks.StructBlock((('menu_title', wagtail.wagtailcore.blocks.CharBlock(help_text='Name of category in footer', max_length=20)), ('menu_category', wagtail.wagtailcore.blocks.PageChooserBlock(target_model=['home.CategoryPage'])), ('menu_items', wagtail.wagtailcore.blocks.static_block.StaticBlock(admin_text="<p>Pages will automatically be displayed as menu items under the following conditions: 1) if they are published as immediate children of this category, and 2) if 'Show in menus' is checked in their edit mode.</p>"))))), ('static_menu', wagtail.wagtailcore.blocks.StructBlock((('menu_title', wagtail.wagtailcore.blocks.CharBlock(max_length=255)), ('menu_items', wagtail.wagtailcore.blocks.StreamBlock((('page', wagtail.wagtailcore.blocks.StructBlock((('title', wagtail.wagtailcore.blocks.CharBlock(max_length=255)), ('page', wagtail.wagtailcore.blocks.PageChooserBlock())))), ('external_link', wagtail.wagtailcore.blocks.StructBlock((('title', wagtail.wagtailcore.blocks.CharBlock(label='Page title', max_length=255)), ('url', wagtail.wagtailcore.blocks.URLBlock())))), ('address', wagtail.wagtailcore.blocks.StructBlock((('name', wagtail.wagtailcore.blocks.CharBlock(max_length=255)), ('street_number', wagtail.wagtailcore.blocks.CharBlock(max_length=255)), ('city_state_zip', wagtail.wagtailcore.blocks.CharBlock(max_length=255)), ('url', wagtail.wagtailcore.blocks.URLBlock()))))))))))), blank=True),
),
migrations.AlterField(
model_name='headermenu',
name='menus',
field=wagtail.wagtailcore.fields.StreamField((('category_menu', wagtail.wagtailcore.blocks.StructBlock((('menu_title', wagtail.wagtailcore.blocks.CharBlock(help_text='Name of category in footer', max_length=20)), ('menu_category', wagtail.wagtailcore.blocks.PageChooserBlock(target_model=['home.CategoryPage'])), ('menu_items', wagtail.wagtailcore.blocks.static_block.StaticBlock(admin_text="<p>Pages will automatically be displayed as menu items under the following conditions: 1) if they are published as immediate children of this category, and 2) if 'Show in menus' is checked in their edit mode.</p>")), ('menu_description', wagtail.wagtailcore.blocks.CharBlock(help_text='Text that appears in the mega-nav above list of pages in this category.', max_length=255)), ('menu_image', wagtail.wagtailcore.blocks.PageChooserBlock(help_text='Display a preview image and link to a page in this category.', target_model=['home.ContentPage']))))), ('static_menu', wagtail.wagtailcore.blocks.StructBlock((('menu_title', wagtail.wagtailcore.blocks.CharBlock(max_length=255)), ('menu_description', wagtail.wagtailcore.blocks.CharBlock(help_text='Text that appears in the mega-nav above list of pages in this category.', max_length=255)), ('menu_image', wagtail.wagtailimages.blocks.ImageChooserBlock(help_text='Display a preview image and link to a page in this category.')), ('menu_image_text', wagtail.wagtailcore.blocks.CharBlock(help_text='Link text displayed below image', max_length=25)), ('menu_image_url', wagtail.wagtailcore.blocks.URLBlock(help_text='Link displayed below image')), ('menu_items', wagtail.wagtailcore.blocks.StreamBlock((('page', wagtail.wagtailcore.blocks.StructBlock((('title', wagtail.wagtailcore.blocks.CharBlock(max_length=255)), ('page', wagtail.wagtailcore.blocks.PageChooserBlock())))), ('external_link', wagtail.wagtailcore.blocks.StructBlock((('title', wagtail.wagtailcore.blocks.CharBlock(label='Page title', max_length=255)), ('url', wagtail.wagtailcore.blocks.URLBlock())))), ('address', wagtail.wagtailcore.blocks.StructBlock((('name', wagtail.wagtailcore.blocks.CharBlock(max_length=255)), ('street_number', wagtail.wagtailcore.blocks.CharBlock(max_length=255)), ('city_state_zip', wagtail.wagtailcore.blocks.CharBlock(max_length=255)), ('url', wagtail.wagtailcore.blocks.URLBlock()))))))))))), blank=True),
),
migrations.RunPython(add_fields, remove_fields),
]
from __future__ import unicode_literals
from django.db import models
from wagtail.wagtailadmin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.wagtailcore.fields import StreamField
from . import blocks
class Navigation(models.Model):
"""Abstract base class for the hierarchical navigation blocks in the header and footer.
"""
site = models.OneToOneField(
'wagtailcore.Site',
default=1,
related_name='%(class)s',
related_query_name='%(class)s',
)
class Meta:
abstract = True
class HeaderMenu(Navigation):
"""Used to provide the parent object for all of the site navigation
menus and menu items in the header.
"""
top_row = StreamField((
('internal_link', blocks.PageLink()),
('external_link', blocks.ExternalLink()),
), blank=True)
menus = StreamField((
('category_menu', blocks.HeaderCategoryMenuBlock()),
('static_menu', blocks.HeaderStaticMenuBlock()),
), blank=True)
panels = [
FieldPanel('site'),
StreamFieldPanel('top_row'),
StreamFieldPanel('menus'),
]
def __str__(self):
return "Header Menu for {}".format(self.site)
class FooterMenu(Navigation):
"""Used to provide the parent object for all of the site navigation
menus and menu items in the footer.
"""
menus = StreamField((
('category_menu', blocks.FooterCategoryMenuBlock()),
('static_menu', blocks.FooterStaticMenuBlock()),
), blank=True)
panels = [
FieldPanel('site'),
StreamFieldPanel('menus'),
]
def __str__(self):
return "Footer Menu for {}".format(self.site)
class Modification(object):
def apply(self, item):
""" Apply a modification to a streamblock item. """
raise NotImplementedError()
class AddField(Modification):
""" Add a field to a StreamValue """
def __init__(self, name, value=None):
self.name = name
self.value = value
def apply(self, item):
if self.name not in item['value'].keys():
item['value'][self.name] = self.value
class RemoveField(Modification):
""" Remove a field from a StreamValue """
def __init__(self, name):
self.name = name
def apply(self, item):
if self.name in item['value'].keys():
item['value'].pop(self.name)
class RenameType(Modification):
""" Change the type of a StreamValue (leave its contents the same). """
def __init__(self, old_type, new_type):
self.type = old_type
self.new_type = new_type
def apply(self, item):
if item['type'] == self.type:
item['type'] = self.new_type
def process(data, mapper):
# A top level stream_block is a list, the children may/may not be
# lists/dicts:
if isinstance(data, list):
stream = []
for item in data:
# This is just plain data, e.g. pks.
if not isinstance(item, dict):
stream.append(item)
continue
# Otherwise, this should be a stream value, so test to see if there
# is a mapper change, and if so, apply all its tasks:
item_dict = item.copy()
item_type = item_dict['type']
if item_type in mapper.keys():
for task in mapper[item_type]:
task.apply(item_dict)
item_dict.update(
value=process(item_dict['value'], mapper)
)
stream.append(item_dict)
return stream
# This almost certainly is a struct block, so process each of its
# fields, potentially remapping a field name.
elif isinstance(data, dict):
new_data = {}
for key, value in data.items():
new_data[key] = process(value, mapper)
return new_data
# Finally, just pass back ordinary values.
else:
return data
def migrate(model, stream_block_name, old_block_definition, new_block_definition, mapper):
""" Migrate a stream_block_name on a model.
Params:
model = Model to apply migration to.
stream_block_name = the name of the streamblock field on the Model.
old_block_definition = stream block structure before migration.
new_block_definition = stream block structure after migration.
mapper = a dictionary mapping fields to a list of Modification instances to apply.
"""
# Swap out the structure of the body field with one that is
# compatible with the data currently in the db.
stream_field = model._meta.get_field(stream_block_name)
# stream_field.stream_block appears not to be versioned - it is always the
# value of the latest migration version.
stream_field.stream_block = old_block_definition
pages = []
for page in model.objects.all():
pages.append(page)
# Convert the value into a plain Python data structure for processing.
stream_block_value = getattr(page, stream_block_name)
data = stream_block_value.stream_block.get_prep_value(stream_block_value)
# Recursively walk the data structure, making the changes
# specified by the mapper.
data = process(data, mapper)
# Switch it back into a tree of objects that can be saved on the field.
setattr(page, stream_block_name, new_block_definition.to_python(data))
# Put in the new structure of the field, and save the modified data back
# into the db.
stream_field.stream_block = new_block_definition
for page in pages:
stream_field.stream_block = new_block_definition
page.save()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment