├── wagtailcontentstream ├── __init__.py ├── tests │ ├── __init__.py │ └── test_models.py ├── templates │ └── wagtailcontentstream │ │ └── blocks │ │ ├── heading.html │ │ ├── section_struct_block.html │ │ └── captioned_image.html ├── models.py ├── wagtail_hooks.py └── blocks.py ├── MANIFEST.in ├── setup.py ├── LICENSE └── README.md /wagtailcontentstream/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wagtailcontentstream/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include wagtailcontentstream/templates * 3 | -------------------------------------------------------------------------------- /wagtailcontentstream/templates/wagtailcontentstream/blocks/heading.html: -------------------------------------------------------------------------------- 1 |

{{ self }}

2 | -------------------------------------------------------------------------------- /wagtailcontentstream/templates/wagtailcontentstream/blocks/section_struct_block.html: -------------------------------------------------------------------------------- 1 |

{{ self.section_heading }}

2 | {{ self.body }} 3 | -------------------------------------------------------------------------------- /wagtailcontentstream/templates/wagtailcontentstream/blocks/captioned_image.html: -------------------------------------------------------------------------------- 1 | {% load wagtailimages_tags %} 2 | 3 | {% spaceless %} 4 |
5 | {% image self.image original %} 6 | {% if self.caption|length %} 7 |
{{ self.caption }}
8 | {% endif %} 9 | {% if self.credit|length %} 10 |
Credit: {{ self.credit }}
11 | {% endif %} 12 |
13 | {% endspaceless %} 14 | -------------------------------------------------------------------------------- /wagtailcontentstream/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.core.management import call_command 2 | 3 | from test_plus.test import TestCase 4 | from wagtail.wagtailcore.models import Page, Site 5 | from wagtail.tests.utils import WagtailPageTests 6 | 7 | from cms.models import ClassroomTool 8 | 9 | 10 | class CMSModelTests(TestCase): 11 | 12 | def setUp(self): 13 | p = Page() 14 | p.title='Root Page' 15 | p.slug='root-page' 16 | p.depth=0 17 | p.save() 18 | 19 | s=Site() 20 | s.root_page=p 21 | s.is_default_site=True 22 | s.hostname='localhost' 23 | s.port=80 24 | s.save() 25 | 26 | 27 | def test_classroom_tool(self): 28 | """ 29 | Test creation of a Classroom Tool. 30 | """ 31 | 32 | # root_page = Site.objects.get(is_default_site=True).root_page 33 | 34 | t = WagtailPageTests() 35 | t.assertCanCreateAt(Page, ClassroomTool) 36 | -------------------------------------------------------------------------------- /wagtailcontentstream/models.py: -------------------------------------------------------------------------------- 1 | from wagtail import VERSION as WAGTAIL_VERSION 2 | 3 | 4 | if WAGTAIL_VERSION >= (3, 0): 5 | from wagtail.admin.panels import FieldPanel 6 | from wagtail.fields import StreamField 7 | from wagtail.models import Page 8 | else: 9 | from wagtail.admin.edit_handlers import StreamFieldPanel as FieldPanel 10 | from wagtail.core.fields import StreamField 11 | from wagtail.core.models import Page 12 | 13 | from .blocks import ContentStreamBlock, ContentStreamBlockWithRawCode, SectionBlock 14 | 15 | 16 | class ContentStreamPage(Page): 17 | body = StreamField( 18 | ContentStreamBlock(), 19 | blank=True, 20 | ) 21 | 22 | content_panels = Page.content_panels + [ 23 | FieldPanel("body"), 24 | ] 25 | 26 | class Meta: 27 | abstract = True 28 | 29 | 30 | class ContentStreamPageWithRawCode(Page): 31 | body = StreamField( 32 | ContentStreamBlockWithRawCode(), 33 | blank=True, 34 | ) 35 | 36 | content_panels = Page.content_panels + [ 37 | FieldPanel("body"), 38 | ] 39 | 40 | class Meta: 41 | abstract = True 42 | 43 | 44 | class SectionContentStreamPage(Page): 45 | body = StreamField( 46 | SectionBlock(), 47 | blank=True, 48 | ) 49 | 50 | content_panels = Page.content_panels + [ 51 | FieldPanel("body"), 52 | ] 53 | 54 | class Meta: 55 | abstract = True 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md") as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name="wagtailcontentstream", 8 | description="Wagtail Content Stream provides a StreamField of standard content types.", 9 | long_description=long_description, 10 | long_description_content_type="text/markdown", 11 | author="Tim Allen", 12 | author_email="tallen@wharton.upenn.edu", 13 | url="https://github.com/FlipperPA/wagtailcontentstream", 14 | include_package_data=True, 15 | packages=find_packages(), 16 | zip_safe=False, 17 | install_requires=[ 18 | "wagtail>=3", 19 | "wagtailcodeblock>=1.14.0.0", 20 | ], 21 | setup_requires=["setuptools_scm"], 22 | use_scm_version=True, 23 | classifiers=[ 24 | "Development Status :: 5 - Production/Stable", 25 | "Environment :: Web Environment", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: BSD License", 28 | "Operating System :: OS Independent", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | "Framework :: Django", 35 | "Framework :: Wagtail", 36 | "Framework :: Wagtail :: 3", 37 | "Topic :: Internet :: WWW/HTTP", 38 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /wagtailcontentstream/wagtail_hooks.py: -------------------------------------------------------------------------------- 1 | from wagtail.admin.rich_text.converters.html_to_contentstate import ( 2 | InlineStyleElementHandler, 3 | ) 4 | from wagtail.admin.rich_text.editors.draftail.features import InlineStyleFeature 5 | from wagtail.core import hooks 6 | 7 | 8 | @hooks.register("register_rich_text_features") 9 | def register_monospace_feature(features): 10 | """ 11 | Registering the `monospace` feature, which uses the `CODE` Draft.js inline style type, 12 | and is stored as HTML with a `` tag. 13 | """ 14 | feature_name = "monospace" 15 | draftail_type = "CODE" 16 | html_tag = "code" 17 | 18 | # Configure how Draftail handles the feature in its toolbar. 19 | control = { 20 | "type": draftail_type, 21 | "label": "{ }", 22 | "description": "Monospace", 23 | } 24 | 25 | # Call register_editor_plugin to register the configuration for Draftail. 26 | features.register_editor_plugin( 27 | "draftail", feature_name, InlineStyleFeature(control) 28 | ) 29 | 30 | # Configure the content transform from the DB to the editor and back. 31 | db_conversion = { 32 | "from_database_format": {html_tag: InlineStyleElementHandler(draftail_type)}, 33 | "to_database_format": {"style_map": {draftail_type: html_tag}}, 34 | } 35 | 36 | # Call register_converter_rule to register the content transformation conversion. 37 | features.default_features.append(feature_name) 38 | features.register_converter_rule("contentstate", feature_name, db_conversion) 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Timothy Allen and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wagtail Content Stream 2 | 3 | An abstract Django model with a Wagtail StreamField named `body` with multiple blocks I use on a regular basis. This is geared towards developers who need to write examples with code in them. It's opinionated: very little HTML is allowed in the text block, forcing authors to create structured data. The following blocks are included in `ContentStreamBlock`: 4 | 5 | * Heading 6 | * Paragraph 7 | * Captioned Image 8 | * Embed 9 | * Table 10 | * Code Block 11 | 12 | A secondary StreamBlock, `ContentStreamBlockWithRawCode`, also provides an additional block for injecting HTML, JS, and CSS code. Use with care, as this can really blow up your markup and is a potential code injection point! 13 | 14 | Three pages types are provided out-of-the-box. 15 | 16 | ## Example Usage 17 | 18 | You will need to add `wagtailcodeblock` to your `INSTALLED_APPS` Django setting. 19 | 20 | #### Basic Usage: a Title Field and Content Stream 21 | 22 | First, create a page type in your `models.py`: 23 | 24 | ```python 25 | from wagtailcontentstream.models import ContentStreamPage, SectionContentStreamPage, ContentStreamPageWithRawCode 26 | 27 | class StandardPage(ContentStreamPage): 28 | pass 29 | 30 | class SectionStandardPage(SectionContentStreamPage): 31 | pass 32 | 33 | class StandardPageWithRawCode(ContentStreamPageWithRawCode): 34 | pass 35 | ``` 36 | 37 | Then in your template: 38 | 39 | ```django 40 | {% load wagtailcore_tags %} 41 | 42 |

{{ page.title }}

43 | {% include_block page.body %} 44 | ``` 45 | 46 | #### Extended Usage: Adding More Fields 47 | 48 | ```python 49 | from django.conf import settings 50 | from django.db import models 51 | from wagtail.admin.edit_handlers import FieldPanel 52 | from wagtailcontentstream.models import ContentStreamPage 53 | 54 | 55 | class StandardPage(ContentStreamPage): 56 | date = models.DateField("Post Date") 57 | authors = models.ManyToManyField(settings.AUTH_USER_MODEL) 58 | 59 | content_panels = [ 60 | FieldPanel('date'), 61 | FieldPanel('authors'), 62 | ] + ContentStreamPage.content_panels 63 | ``` 64 | 65 | # Release Notes & Contributors 66 | 67 | * Thank you to our [wonderful contributors](https://github.com/FlipperPA/wagtailcontentstream/graphs/contributors)! 68 | * Release notes are [available on GitHub](https://github.com/FlipperPA/wagtailcontentstream/releases). 69 | 70 | # Project Maintainer 71 | 72 | * Timothy Allen (https://github.com/FlipperPA) 73 | -------------------------------------------------------------------------------- /wagtailcontentstream/blocks.py: -------------------------------------------------------------------------------- 1 | from wagtail import VERSION as WAGTAIL_VERSION 2 | from wagtail.contrib.table_block.blocks import TableBlock 3 | 4 | if WAGTAIL_VERSION >= (3, 0): 5 | from wagtail.blocks import ( 6 | ChoiceBlock, 7 | RichTextBlock, 8 | TextBlock, 9 | StructBlock, 10 | StreamBlock, 11 | ) 12 | from wagtail.documents.blocks import DocumentChooserBlock 13 | from wagtail.embeds.blocks import EmbedBlock 14 | from wagtail.images.blocks import ImageChooserBlock 15 | else: 16 | from wagtail.core.blocks import ( 17 | ChoiceBlock, 18 | RichTextBlock, 19 | TextBlock, 20 | StructBlock, 21 | StreamBlock, 22 | ) 23 | from wagtail.documents.blocks import DocumentChooserBlock 24 | from wagtail.embeds.blocks import EmbedBlock 25 | from wagtail.images.blocks import ImageChooserBlock 26 | 27 | from wagtailcodeblock.blocks import CodeBlock 28 | 29 | 30 | class CaptionedImageBlock(StructBlock): 31 | """ 32 | An image block with a caption, credit, and alignment. 33 | """ 34 | 35 | image = ImageChooserBlock( 36 | help_text="The image to display.", 37 | ) 38 | caption = TextBlock( 39 | required=False, help_text="The caption will appear under the image, if entered." 40 | ) 41 | credit = TextBlock( 42 | required=False, help_text="The credit will appear under the image, if entered." 43 | ) 44 | align = ChoiceBlock( 45 | choices=[ 46 | ("left", "Left"), 47 | ("right", "Right"), 48 | ("center", "Center"), 49 | ("full", "Full Width"), 50 | ], 51 | default="left", 52 | help_text="How to align the image in the body of the page.", 53 | ) 54 | 55 | class Meta: 56 | icon = "image" 57 | template = "wagtailcontentstream/blocks/captioned_image.html" 58 | help_text = "Select an image and add a caption (optional)." 59 | 60 | 61 | class ContentStreamBlock(StreamBlock): 62 | """ 63 | Contains the elements we'll want to have in a Content Stream. 64 | """ 65 | 66 | heading = TextBlock( 67 | icon="title", 68 | template="wagtailcontentstream/blocks/heading.html", 69 | ) 70 | paragraph = RichTextBlock( 71 | icon="pilcrow", 72 | features=["bold", "italic", "link", "ol", "ul", "monospace"], 73 | ) 74 | image = CaptionedImageBlock() 75 | document = DocumentChooserBlock() 76 | embed = EmbedBlock(icon="media") 77 | table = TableBlock(icon="table") 78 | code = CodeBlock(icon="code") 79 | 80 | class Meta: 81 | help_text = "The main page body." 82 | 83 | 84 | class ContentStreamBlockWithRawCode(ContentStreamBlock): 85 | raw_code = CodeBlock( 86 | icon="code", 87 | language="html", 88 | template="wagtailcodeblock/raw_code.html", 89 | ) 90 | 91 | 92 | class SectionStructBlock(StructBlock): 93 | """ 94 | Contains the elements we'll want to have in a Sectioned Content Stream block. 95 | """ 96 | 97 | section_heading = TextBlock( 98 | icon="title", 99 | help_text="Heading for this section.", 100 | ) 101 | body = ContentStreamBlock( 102 | help_text="The body content goes here.", 103 | ) 104 | 105 | class Meta: 106 | template = "wagtailcontentstream/blocks/section_struct_block.html" 107 | icon = "doc-full-inverse" 108 | 109 | 110 | class SectionBlock(StreamBlock): 111 | """ 112 | Streamblock to associate multiple blocks with a section. 113 | """ 114 | 115 | section = SectionStructBlock() 116 | 117 | class Meta: 118 | help_text = "The main page body." 119 | --------------------------------------------------------------------------------