0
votes

I have been trying to learn Wagtail and the intricacies of using StreamFields and blocks are giving me a headache. I had no problems following the Wagtail docs and the demo app, but going beyond that seems to be insurmountable without external help.

Here is what I am trying (and failing) to achieve.

When playing around with the Blog application, I tried to extend it so the StreamField allows for adding of code blocks with syntax highlighting, based on pygments. The source code is managed by a custom StructBlock class (CodeBlock, naturally) and is part of the StreamField body of the BlogPage. In Wagtail admin I can enter code, used language, what highlight style to apply and whether to show line numbers or not. This all works perfectly fine, up to the point where I would like to select the additional stylesheet for the rendering template based on the selected highlight style. Here is how the page template would include the stylesheet:

{% block extra_css %}
    {# This goes in the page <head> section #}
    {% if has_code_block %}
        {% if code_colorizer %}
            <link rel="stylesheet" type="text/css" href="{% static 'css/highlight_{{ code_colorizer }}.css' %}">
        {% else %}
            <link rel="stylesheet" type="text/css" href="{% static 'css/highlight_default.css' %}">
        {% endif %}
    {% endif %}
{% endblock %}

The CodeBlock does its job in the render method (idea gratefully found - and shamelessly used - online). At this point in the app flow it's too late to add the highlight style to the page context, so I tried to decompose the page body field in an overridden def get_context function, without success. I just haven't been able to get to the underlying JSON text or access the data through any of the class members from withing the BlogPage class.

Is there a way to add the highlight style to the page context just after the data is loaded from the DB and before given over to the template?

Here is my basic blog page:

class BlogPage(Page):
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
    posted_date = models.DateField("Post date")
    edited_date = models.DateField("Edited date", null=True, blank=True)
    feed_image = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+')
    body = StreamField(BlogStreamBlock)

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

    subpage_types = []

    parent_page_types = ['BlogIndexPage']

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

BlogPage.content_panels = [
    FieldPanel('title', classname='full title'),
    FieldPanel('posted_date'),
    FieldPanel('edited_date'),
    StreamFieldPanel('body'),
    InlinePanel('related_links', label="Related links"),
]

BlogPage.promote_panels = Page.promote_panels + [
    ImageChooserPanel('feed_image'),
    FieldPanel('tags'),
]

This is the definition of my BlogStreamBlock class:

class BlogStreamBlock(StreamBlock):
    subtitle = CharBlock(icon='title', classname='title')
    abstract = RichTextBlock(icon='pilcrow')
    paragraph = RichTextBlock()
    aligned_image = ImageBlock()
    source_code = CodeBlock()
    quote = QuoteBlock()

And finally, here is the CodeBlock class:

class CodeBlock(StructBlock):
    LANGUAGE_CHOICES = (
        ('aspx-cs', '.NET ASP/C#'),
        ('aspx-vb', '.NET ASP/VisualBasic'),
        ('csharp', '.NET C#'),
        ('fsharp', '.NET F#'),
        ('vbnet', '.NET VisualBasic'),
        ('ng2', 'Angular 2'),
        ('html+ng2', 'Angular 2 Html'),
        ('apache', 'Apache Config'),
        ('arduino', 'Arduino Sketch'),
        ('asm', 'Assembly'),
        ('bash', 'Bash Shell'),
        ('batch', 'Batch CMD File'),
        ('c', 'C'),
        ('cpp', 'C++'),
        ('cmake', 'CMake'),
        ('coffeescript', 'Coffee Script'),
        ('css', 'CSS'),
        # ... and many, many more ...
        ('vhdl', 'Vhdl'),
        ('registry', 'Windows Registry'),
        ('xml', 'XML'),
        ('xml+php', 'XML/PHP'),
        ('xslt', 'XSLT'),
        ('yaml', 'Yaml'),
    )

    COLORIZER_CHOICES = (
        ('abap', 'Abap'),
        ('algol', 'Algol'),
        ('algol_nu', 'Algol Nu'),
        # ... finish the list with all the highlight styles in the current version of pygments
        ('vs', 'Vs'),
        ('xcode', 'Xcode'),
    )

    language = ChoiceBlock(choices=LANGUAGE_CHOICES, classname='full')
    colors = ChoiceBlock(choices=COLORIZER_CHOICES, classname='full')
    code = TextBlock()
    line_numbers = BooleanBlock(classname='full')

    class Meta:
        icon = 'code'

    def render(self, value, context=None):
        src = value['code'].strip('\n');
        lang = value['language']
        line_nos = value['line_numbers']

        lexer = get_lexer_by_name(lang)
        formatter = get_formatter_by_name('html', linenos='table' if line_nos else False, cssclass='codehilite', style='default',noclasses=False)
        return mark_safe(highlight(src, lexer, formatter))
1
Please can you give more details about what you've already tried and failed to achieve within the get_context method? Accessing self.body should definitely give you the data you're interested in.gasman
Thanks, my error was that I forgot to publish the page, so the new blocks were not included when I looped over the body blocks...Todor Todorov

1 Answers

0
votes

It is amazing how a few hours of sleep help to clarify your thought process! Sorry to have wasted your time.

It is not enough to save your page in the admin editor! You have to publish it, as well!

As gasman suggested in his comment, overriding get_context within the BlogPage gives you direct access to the body class member. There I can loop over the elements, check their block_type and access their subelements like so:

def get_context(self, request, *args, **kwargs):
    context = super(BlogPage, self).get_context(request)
    if self.body and len(self.body) > 0:
        for block in self.body:
            if block.block_type == 'source_code':
                context['has_code_block'] = True
                context['code_colorizer'] = block.value['colors'] if block.value['colors'] else 'default'
    return context

This will make sure that a CSS stylesheet is always available for when there is a block of source code on the page.

Now let's address the other glaring error in my code above. The template code for setting the stylesheet does not work as posted. What it produces is some URL-escaped text like:

<link rel="stylesheet" type="text/css" href="/static/css/highlight_%7B%7B%20code_colorizer%20%7D%7D.css">

What it actually needs to be is the following:

{% block extra_css %}
    {% if has_code_block %}
        {% if code_colorizer %}
            {% with 'css/highlight_'|add:code_colorizer|add:'.css' as colorizer_choice %}
                <link rel="stylesheet" type="text/css" href="{% static colorizer_choice %}">
            {% endwith %}
        {% else %}
            <link rel="stylesheet" type="text/css" href="{% static 'css/highlight_default.css' %}">
        {% endif %}
    {% endif %}
{% endblock %}

Actually, since the get_context function always sets 'default' colorizer, if the user doesn't select one, the {% if code_colorizer %} check and its {% else %} branch can be removed entirely.

For anyone who wants to use this code, be aware that having multiple code blocks on the same page using different syntax highlighting style will not work as expected. As it is, the page class includes only the selected stylesheet of the last code block. I could add all different CSS files, if that has been selected in each code block, but since the pygments highlighter uses the same CSS class names, having multiple styles in the HTML file will still not work as you want it to. Of course, one can use the pygments API to produce an enclosing <div></div> tag with a specific class for each highlight style (in the render function of the CodeBlock) and then edit the corresponding CSS files to prepend this same class to each element, but this is beyond the scope of this question.