Esempio n. 1
0
class NavGroup(blocks.StructBlock):
    draft = blocks.BooleanBlock(
        required=False,
        default=False,
        help_text='If checked, this block will only show '
        'on our sharing site (Content).',
        label='Mark block as draft'
    )
    group_title = blocks.CharBlock(
        required=False,
        label='Column title')
    hide_group_title = blocks.BooleanBlock(
        required=False,
        label='Hide column title',
        help_text='If column shares title with previous column, '
                  'enter title text above but check this box so title '
                  'only shows in first column.')
    nav_items = blocks.ListBlock(
        NavItem(),
        required=False,
        label='Menu items')
Esempio n. 2
0
class ItemIntroduction(blocks.StructBlock):
    show_category = blocks.BooleanBlock(
        required=False,
        default=True,
        help_text=(
            "Whether to show the category or not "
            "(category must be set in 'Configuration')."
        )
    )

    heading = blocks.CharBlock(required=False)
    paragraph = blocks.RichTextBlock(required=False)

    date = blocks.DateBlock(required=False)
    has_social = blocks.BooleanBlock(
        required=False, help_text="Whether to show the share icons or not.")

    class Meta:
        icon = 'form'
        template = '_includes/organisms/item-introduction.html'
        classname = 'block__flush-top'
Esempio n. 3
0
class LandingPage(Page):
    body = StreamField([
        ('heading', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock(icon="image")),
        ('two_columns', TwoColumnBlock()),
        ('embedded_video', EmbedBlock(icon="media")),
    ],null=True,blank=True)

    content_panels = Page.content_panels + [
        StreamFieldPanel('body'),
    ]

    @property
    def blog_page(self):
        return self.get_parent().specific

    def get_context(self, request, *args, **kwargs):
        context = super(LandingPage, self).get_context(request, *args, **kwargs)
        context['blog_page'] = self.blog_page
        return context
    def test_include_block_tag_with_filtered_value(self):
        """
        The block parameter on include_block tag should support complex values including filters,
        e.g. {% include_block foo|default:123 %}
        """
        block = blocks.CharBlock(template='tests/jinja2/heading_block.html')
        bound_block = block.bind('bonjour')

        result = render_to_string(
            'tests/jinja2/include_block_test_with_filter.html', {
                'test_block': bound_block,
                'language': 'fr',
            })
        self.assertIn('<body><h1 lang="fr">bonjour</h1></body>', result)

        result = render_to_string(
            'tests/jinja2/include_block_test_with_filter.html', {
                'test_block': None,
                'language': 'fr',
            })
        self.assertIn('<body>999</body>', result)
Esempio n. 5
0
class YoutubePage(Page):
    abstract = RichTextField()
    body = StreamField([
        ('rich_text', blocks.RichTextBlock()),
        ('heading', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.RichTextBlock()),
        ('raw_html', blocks.RawHTMLBlock())
    ])
    youtube_video_id = models.CharField(max_length=20)
    social_image = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+')


    content_panels = Page.content_panels + [
            FieldPanel('abstract', classname="full"),
            StreamFieldPanel('body'),
            FieldPanel('youtube_video_id'),
        ]

    promote_panels = Page.promote_panels + [
            ImageChooserPanel('social_image')
        ]
Esempio n. 6
0
class LatestNewsBlock(CountryRegionStructBlock):
    limit = blocks.CharBlock()

    class Meta:
        icon = 'fa fa-list'
        label = 'Latest news'
        template = 'widgets/latest-news.html'

    def get_context(self, value):
        context = super().get_context(value)
        queryset = BlogPage.objects.all()
        if self.country or self.region:
            tag = self.country.slug or self.region.slug
            filter_queryset = queryset.filter(tags__slug=tag)
            if filter_queryset.count() > 0:
                queryset = filter_queryset
            else:
                queryset = queryset.filter(tags__isnull=True)
        limit = value.get('limit')
        context['news'] = queryset[:int(limit)]
        return context
Esempio n. 7
0
class LinkBlock(StructBlock):
    cls = blocks.ChoiceBlock(choices=[
        ('btn', 'Button'),
    ],
                             required=False,
                             label='Type')
    url = blocks.URLBlock(label='URL')
    text = blocks.CharBlock()

    class Meta:
        icon = 'anchor'
        template = 'widgets/link.html'

    def get_context(self, value):
        context = super().get_context(value)
        context['href'] = value.get('url')
        context['text'] = value.get('text')
        context['class'] = value.get('cls')
        return context

    ('link', URLBlock(icon="link")),
Esempio n. 8
0
class LinkBlock(blocks.StructBlock):
    title = blocks.CharBlock()
    url = blocks.URLBlock()

    def get_context(self, value):
        context = super(LinkBlock, self).get_context(value)
        context['classname'] = 'important' if value[
            'title'] == 'Torchbox' else 'normal'
        return context

    def get_form_context(self, value, prefix='', errors=None):
        context = super(LinkBlock, self).get_form_context(value,
                                                          prefix=prefix,
                                                          errors=errors)
        context['extra_var'] = "Hello from get_form_context!"
        return context

    class Meta:
        icon = "site"
        template = 'tests/blocks/link_block.html'
        form_template = 'tests/block_forms/link_block.html'
Esempio n. 9
0
class MediaTextOverlayBlock(blocks.StructBlock):
    title = blocks.CharBlock(required=False,
                             label="Title Text",
                             max_length=255,
                             help_text="Appears above the module.")
    image = ImageChooserBlock()
    logo = ImageChooserBlock(label="Title Logo", required=False)
    text = blocks.RichTextBlock(max_length=75,
                                required=False,
                                features=[
                                    "bold", "italic", "ol", "ul", "link",
                                    "document-link", "justify"
                                ])
    link = LinkBlock(required=False)
    customisation = CustomisationBlock(required=False)

    def clean(self, value):
        if value['title'] and value['logo']:
            error_messages = ["Please choose only one of logo or title."]
            raise ValidationError(
                "Validation error in MediaTextOverlayBlock",
                params={
                    'title': error_messages,
                    'logo': error_messages
                },
            )
        if not value['title'] and not value['logo']:
            error_messages = ["Please choose a logo or title."]
            raise ValidationError(
                "Validation error in MediaTextOverlayBlock",
                params={
                    'title': error_messages,
                    'logo': error_messages
                },
            )
        return super().clean(value)

    class Meta:
        icon = "image"
        template = "blocks/media_text_overlay_block.html"
Esempio n. 10
0
class SolutionsBlock(StructBlock):
    h5 = blocks.CharBlock(required=False)
    h2 = blocks.CharBlock(required=False)
    color = ChoiceBlock(choices=[
        ('', 'Блок светлый или тёмный?'),
        ('light', 'Светлый'),
        ('dark', 'Тёмный')
    ], blank=True, required=False)
    btntext = blocks.CharBlock(required=False)
    formh3 = blocks.CharBlock(required=False)
    button = blocks.CharBlock(required=False)
    handle = blocks.CharBlock()

    class Meta:
        icon = 'doc-empty-inverse'
        template = "sections/solutions_block.html"
Esempio n. 11
0
class BlogPage(Page):
    date = models.DateField("Post date", default=timezone.now)
    intro = models.CharField(max_length=250, blank=True)
    main_feature = models.BooleanField(default=False)
    body = StreamField([
        ('heading', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.RichTextBlock()),        
        ('image', ImageChooserBlock()),
        ('articleIP', blocks.URLBlock()),
    ])    
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
    categories = ParentalManyToManyField('blog.BlogCategory', blank=True)
    authors = models.CharField(max_length=255, default="Buckanjären")
            
    def main_image(self):
        gallery_item = self.gallery_images.first()        
        if gallery_item:
            return gallery_item.image
        else:
            print("--- no image")
            return None

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]
    

    content_panels = Page.content_panels + [
        MultiFieldPanel([
            FieldPanel('date'),
            FieldPanel('tags'),
            FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
            FieldPanel('main_feature', widget=forms.CheckboxSelectMultiple),
        ], heading="Blog information"),
        FieldPanel('intro'),
        FieldPanel('authors'),
        StreamFieldPanel('body'),
        InlinePanel('gallery_images', label="Gallery images"),
    ]
class StandardPage(Page):
    feature_image = models.ForeignKey('wagtailimages.Image',
                                      null=True,
                                      blank=True,
                                      on_delete=models.SET_NULL,
                                      related_name='+')
    feature_image_large = models.BooleanField(default=False)
    body = StreamField([
        ('heading',
         blocks.CharBlock(classname='full title',
                          template='blocks/heading.html')),
        ('section', SectionBlock()),
        ('image', ImageTextBlock(template='blocks/image_text.html')),
        ('quote', PullQuoteBlock(template='blocks/quote.html')),
        ('call_to_action', CallToActionBlock()),
    ])

    content_panels = Page.content_panels + [
        ImageChooserPanel('feature_image'),
        FieldPanel('feature_image_large'),
        StreamFieldPanel('body'),
    ]
Esempio n. 13
0
class FilterControls(BaseExpandable):
    form_type = blocks.ChoiceBlock(choices=[
        ('filterable-list', 'Filterable List'),
        ('pdf-generator', 'PDF Generator'),
    ], default='filterable-list')
    title = blocks.BooleanBlock(default=True, required=False,
                                label='Filter Title')
    post_date_description = blocks.CharBlock(default='Published')
    categories = blocks.StructBlock([
        ('filter_category',
         blocks.BooleanBlock(default=True, required=False)),
        ('show_preview_categories',
         blocks.BooleanBlock(default=True, required=False)),
        ('page_type', blocks.ChoiceBlock(
            choices=ref.filterable_list_page_types,
            required=False
        )),
    ])
    topics = blocks.BooleanBlock(default=True, required=False,
                                 label='Filter Topics')
    authors = blocks.BooleanBlock(default=True, required=False,
                                  label='Filter Authors')
    date_range = blocks.BooleanBlock(default=True, required=False,
                                     label='Filter Date Range')
    output_5050 = blocks.BooleanBlock(default=False, required=False,
                                      label="Render preview items as 50-50s")
    link_image_and_heading = blocks.BooleanBlock(
        default=False,
        required=False,
        help_text='Add links to post preview images and'
                  ' headings in filterable list results'
    )

    class Meta:
        label = 'Filter Controls'
        icon = 'form'

    class Media:
        js = ['filterable-list-controls.js']
Esempio n. 14
0
class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = StreamField([
        ('heading', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
        ('embed', EmbedBlock()),
        ('rawHtml', blocks.RawHTMLBlock()),
    ])

    feed_image = models.ForeignKey('wagtailimages.Image',
                                   null=True,
                                   blank=True,
                                   on_delete=models.SET_NULL,
                                   related_name='+')

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        StreamFieldPanel('body'),
    ]

    promote_panels = [
        MultiFieldPanel(Page.promote_panels, "Common page configuration"),
        ImageChooserPanel('feed_image'),
    ]

    def get_fields(self):
        return [(field.name, field.value_to_string(self))
                for field in BlogPage._meta.fields]

    def cur_site_id(self):
        return "{}".format(self.get_url_parts()[0])
Esempio n. 15
0
    def test_block_render_result_is_safe(self):
        """
        Ensure that any results of template rendering in block.render are marked safe
        so that they don't get double-escaped when inserted into a parent template (#2541)
        """
        stream_block = blocks.StreamBlock([
            ('paragraph',
             blocks.CharBlock(template='tests/jinja2/paragraph.html'))
        ])

        stream_value = stream_block.to_python([
            {
                'type': 'paragraph',
                'value': 'hello world'
            },
        ])

        result = render_to_string('tests/jinja2/stream.html', {
            'value': stream_value,
        })

        self.assertIn('<p>hello world</p>', result)
Esempio n. 16
0
class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=255)
    body = RichTextField(blank=True)
    blogElement = StreamField([
        ('heading', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.TextBlock()),
        ('picture', ImageChooserBlock()),
    ],
                              default=[])

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body', classname="full"),
        StreamFieldPanel('blogElement'),
    ]
Esempio n. 17
0
class ThesisIndexPage(Page):
    header = StreamField([
        ('rawHtml', blocks.RawHTMLBlock()),
        ('heading', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
        ('embed', EmbedBlock()),

    ])

    background_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )

    content_panels = Page.content_panels + [
        StreamFieldPanel('header'),
        ImageChooserPanel('background_image'),
    ]
Esempio n. 18
0
class BlogEntry(OpenGraphMixin, Page):
    """Page for a single blog entry."""

    blog_date = models.DateField("Blog Entry Date")
    blog_author = models.CharField(max_length=255)
    blog_tags = ClusterTaggableManager(through=BlogEntryTag, blank=True)

    body = StreamField([
        ('banner_image', common_blocks.BannerImage()),
        ('heading', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', common_blocks.CaptionImageBlock()),
        ('h1', common_blocks.HeaderH1(classname="full title")),
        ('subhead', common_blocks.Subhead(classname="full title")),
        ('block_quote', common_blocks.BlockQuote()),
        ('call_to_action', common_blocks.CallToAction()),
        ('small_call_to_action', common_blocks.CTAButton()),
    ])

    search_fields = Page.search_fields + [
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        MultiFieldPanel([
            FieldPanel('blog_date'),
            FieldPanel('blog_author'),
            FieldPanel('blog_tags'),
        ],
                        heading="Blog information"),
        StreamFieldPanel('body'),
    ]

    def save(self, *args, **kwargs):
        """Override to have a more specific slug w/ date & title."""
        self.slug = "{0}-{1}".format(self.blog_date.strftime("%Y-%m-%d"),
                                     slugify(self.title))
        super().save(*args, **kwargs)
Esempio n. 19
0
class ImageBasic(blocks.StructBlock):
    upload = ImageChooserBlock(required=False)
    alt = blocks.CharBlock(
        required=False,
        help_text='If the image is decorative (i.e., if a screenreader '
        'wouldn\'t have anything useful to say about it), leave the '
        'Alt field blank.')

    def __init__(self, required=True):
        self.is_required = required
        super(ImageBasic, self).__init__()

    @property
    def required(self):
        return self.is_required

    def clean(self, data):
        error_dict = {}

        try:
            data = super(ImageBasic, self).clean(data)
        except ValidationError as e:
            error_dict.update(e.params)

        if not self.required and not data['upload']:
            return data

        if not data['upload']:
            error_dict.update({'upload': is_required("Upload")})

        if error_dict:
            raise ValidationError("ImageBasic validation errors",
                                  params=error_dict)
        else:
            return data

    class Meta:
        icon = 'image'
class BlogPage(Page):
    intro = RichTextField()
    body = StreamField([
        ('heading', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
    ])
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
    date = models.DateField("Post date")
    feed_image = models.ForeignKey('wagtailimages.Image',
                                   null=True,
                                   blank=True,
                                   on_delete=models.SET_NULL,
                                   related_name='+')

    search_fields = Page.search_fields + [
        index.SearchField('body'),
    ]

    @property
    def blog_index(self):
        # Find closest ancestor which is a blog index
        return self.get_ancestors().type(BlogIndexPage).last()
Esempio n. 21
0
    def test_render_within_structblock(self, get_embed):
        """
        When rendering the value of an EmbedBlock directly in a template
        (as happens when accessing it as a child of a StructBlock), the
        proper embed output should be rendered, not the URL.
        """
        get_embed.return_value = Embed(html='<h1>Hello world!</h1>')

        block = blocks.StructBlock([
            ('title', blocks.CharBlock()),
            ('embed', EmbedBlock()),
        ])

        block_val = block.to_python({'title': 'A test', 'embed': 'http://www.example.com/foo'})

        temp = template.Template('embed: {{ self.embed }}')
        context = template.Context({'self': block_val})
        result = temp.render(context)

        self.assertIn('<h1>Hello world!</h1>', result)

        # Check that get_embed was called correctly
        get_embed.assert_any_call('http://www.example.com/foo')
Esempio n. 22
0
class GeneralPage(Page):
    body = StreamField([
        ('heading', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
        ('html', RawHTMLBlock()),
    ])

    api_fields = (
        'title',
        'body',
    )

    content_panels = [
        FieldPanel('title'),
        StreamFieldPanel('body'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),
    ]
Esempio n. 23
0
class Notification(blocks.StructBlock):
    type = blocks.ChoiceBlock(choices=[
        ('default', 'Default'),
        ('success', 'Success'),
        ('warning', 'Warning'),
        ('error', 'Error'),
    ],
                              required=True,
                              default='default')
    message = blocks.CharBlock(
        required=True, help_text='The main notification message to display.')
    explanation = blocks.TextBlock(
        required=False,
        help_text='Explanation text appears below the message in smaller type.'
    )
    links = blocks.ListBlock(
        atoms.Hyperlink(required=False),
        required=False,
        help_text='Links appear on their own lines below the explanation.')

    class Meta:
        icon = 'warning'
        template = '_includes/molecules/notification.html'
Esempio n. 24
0
class XBlockVideoBlock(blocks.StructBlock):
    """XBlockVideoBlock component"""
    video = VideoChooserBlock(required=True)
    title = blocks.CharBlock(required=False, help_text='Override video title')

    def get_title(self, value):
        return value.get('title')

    def get_video(self, value):
        return value.get(STREAM_DATA_VIDEO_FIELD)

    def get_searchable_content(self, value):
        return [self.get_title(value)]

    def get_api_representation(self, value, context=None):
        block_title = self.get_title(value)
        video = self.get_video(value)

        if video:
            return {
                'video_id': video.id,
                'title': block_title if block_title else video.display_name,
                'view_url': video.view_access_url,
                'transcript_url': video.transcript_url,
                'span_id': get_span_id(VIDEO_BLOCK_TYPE, video.id),
            }

        log.warning(
            'Missing Video: video has been deleted but still referenced in page'
        )
        return {
            'video_id': 0,
            'title': block_title if block_title else "Missing Video",
            'view_url': '',
            'transcript_url': '',
            'span_id': 'missing-video',
        }
Esempio n. 25
0
class BlogPage(Page):

    main_image = models.ForeignKey(ExtendedImage,
                                   null=True,
                                   blank=True,
                                   on_delete=models.SET_NULL,
                                   related_name='+')

    date = models.DateField('Post Date')
    intro = models.CharField(max_length=250)
    body = StreamField([
        ('heading', blocks.CharBlock(classname='full title')),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
        ('html', RawHTMLBlock()),
        ('embed', EmbedBlock()),
    ])

    search_fields = Page.search_fields + (
        index.SearchField('intro'),
        index.SearchField('body'),
    )

    def get_absolute_url(self):
        return self.full_url

    @property
    def blog_index(self):
        return self.get_ancestors().type(BlogIndexPage).last()

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        ImageChooserPanel('main_image'),
        FieldPanel('intro'),
        StreamFieldPanel('body'),
        InlinePanel('related_links', label='Related Links')
    ]
class NavPage(Page):

    # Database fields
    nav_title = RichTextField(default='')
    body = StreamField([('heading', blocks.CharBlock(classname="full title")),
                        ('paragraph', blocks.RawHTMLBlock())])
    date = models.DateField("Post date")
    feed_image = models.ForeignKey('wagtailimages.Image',
                                   null=True,
                                   blank=True,
                                   on_delete=models.SET_NULL,
                                   related_name='+')

    # Search index configuraiton

    search_fields = Page.search_fields + (
        index.SearchField('body'),
        index.FilterField('date'),
    )

    # Editor panels configuration

    content_panels = Page.content_panels + [
        FieldPanel('nav_title'),
        FieldPanel('date'),
        StreamFieldPanel('body'),
    ]

    promote_panels = [
        MultiFieldPanel(Page.promote_panels, "Common page configuration"),
        ImageChooserPanel('feed_image'),
    ]

    # Parent page / subpage type rules

    parent_page_types = ['SectionPage']
Esempio n. 27
0
class MapBlock(blocks.StructBlock):
    marker_title = blocks.CharBlock(
        max_length=120,
        default="Marker Title 'This will be updated after you save changes.'")
    marker_description = blocks.RichTextBlock()
    zoom_level = blocks.IntegerBlock(min_value=0,
                                     max_value=18,
                                     default='2',
                                     required=False)
    location_x = blocks.FloatBlock(default='35.0', required=False)
    location_y = blocks.FloatBlock(default='0.16', required=False)
    marker_x = blocks.FloatBlock(default='51.5', required=False)
    marker_y = blocks.FloatBlock(default='-0.09', required=False)

    @property
    def media(self):
        return forms.Media(
            js=["https://unpkg.com/[email protected]/dist/leaflet.js"],
            css={'all': ["https://unpkg.com/[email protected]/dist/leaflet.css"]})

    class Meta:
        form_template = 'wagtail_blocks/admin_blocks/map.html'
        template = 'wagtail_blocks/map.html'
        icon = "fa-globe"
Esempio n. 28
0
class DropShadowBlock(blocks.StructBlock):
    drop_shadow_is_on = blocks.BooleanBlock(
        label="Drop Shadow Toggle",
        help_text="Show or hide drop shadow",
        required=False)
    text_hex = blocks.CharBlock(label="Text Hex Code",
                                max_length=7,
                                required=False)

    def clean(self, value):
        value = super().clean(value)

        hex_fields = ['text_hex']
        errors = {
            field: ['Please enter a valid hex code']
            for field in hex_fields if not validate_hex(value[field])
        }

        if errors:
            raise ValidationError(
                "Validation error in DropShadowBlock",
                params=errors,
            )
        return value
Esempio n. 29
0
class PDFBlock(blocks.StructBlock):
    """PDFBlock component"""
    doc = DocumentChooserBlock()
    title = blocks.CharBlock(required=False,
                             help_text='Override document title')

    def get_title(self, value):
        return value.get('title')

    def get_doc(self, value):
        return value.get(STREAM_DATA_DOC_FIELD)

    def get_searchable_content(self, value):
        return [self.get_title(value)]

    def get_api_representation(self, value, context=None):
        block_title = self.get_title(value)
        document = self.get_doc(value)

        if document:
            return {
                'doc_id': document.id,
                'title': block_title if block_title else document.title,
                'url': document.file.url,
                'span_id': get_span_id(PDF_BLOCK_TYPE, document.id),
            }

        log.warning(
            'Missing Document: document has been deleted but still referenced in page'
        )
        return {
            'doc_id': 0,
            'title': block_title if block_title else "Missing Document",
            'url': '',
            'span_id': 'missing-document',
        }
Esempio n. 30
0
class TemplatePage(Page):
	"""
	More of a traditional template page type with only one section with stream fileds.  
	the top and bottom rtf blocks are required    
	"""
	top = RichTextField(blank=True)

	middle = StreamField([
		('heading', blocks.CharBlock(classname="full title")),
		('paragraph', blocks.RichTextBlock()),
		('image', ImageChooserBlock()),
		('halves', Halves(classname="full title")),
		('person', PersonBlock()),
		('list', ListItemBlock()),
		('thirds', Thirds())
	])

	bottom = RichTextField(blank=True)

	content_panels = Page.content_panels + [
		FieldPanel('top'),
		StreamFieldPanel('middle'),
		FieldPanel('bottom'),
	]