Last active
September 10, 2018 15:00
-
-
Save dsummersl/b20857017cc30343660e82b6f64f1243 to your computer and use it in GitHub Desktop.
wagtail streamblocks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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), | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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