Search code examples
wagtailwagtail-streamfield

Storing both content and pre-rendered content in a StructBlock


In order to avoid rendering markdown each time a page was viewed, I had two TextFields in my Page model. One in which to enter the markdown in the admin site and a second hidden TextField in which I stored the rendered html during Page save.

Now, I'm trying to add similar functionality to a Markdown StructBlock for use in StreamField. Currently, I'm rendering the html on every page view. AIUI the instances of Blocks are stored as (name, value) tuples.

Is it feasible/sensible to add a third 'rendered' element in the stored tuple plus override methods in some new derived Block class?

Or is this something possible and better addressed with (partial) page caching?


Solution

  • Within the database, a StructBlock is stored as a dict, so if your markdown block is based on StructBlock, adding an extra key to that dict would make most sense. (You're partly correct that items within a StreamBlock are often represented as (name, value) tuples, but making use of that would involve digging deeply into the interaction between StreamBlock and its children, and I think that would overcomplicate things here.)

    All blocks implement a pair of methods get_prep_value(value) (which is used when writing the block to the database, and converts the native Python value of a block to a JSON-serialisable version that can be stored in the database) and to_python(value) (which converts it back again on retrieving it from the database). With that in mind, there are two steps to implement here:

    • override get_prep_value to generate the rendered version and insert that into the database representation
    • override to_python to ensure that the rendered version is preserved when unpacking the database representation (which won't happen automatically, because StructBlock will only unpack the child blocks it knows about, and the rendered HTML value isn't one of them)

    There's an extra complication with the second part, because StructBlock implements a bulk_to_python(values) method as a performance optimisation - this unpacks a list of values in one go, to take advantage of any child blocks that are handled more efficiently that way (such as ones that need database lookups), and bypasses the usual to_python method, so you'll need to override that one too.

    Assuming your starting point is something like:

    class MarkdownBlock(StructBlock):
        markdown_text = TextBlock()
        some_other_property = CharBlock()
    

    you want something like:

    class MarkdownBlock(StructBlock):
        markdown_text = TextBlock()
        some_other_property = CharBlock()
    
        def get_prep_value(self, value):
            value_to_save = super().get_prep_value(value)
            value_to_save['rendered_text'] = render_markdown(value_to_save['markdown_text'])
            return value_to_save
    
        def to_python(self, value):
            python_val = super().to_python(value)
            python_val['rendered_text'] = value['rendered_text']
            return python_val
    
        def bulk_to_python(self, values):
            python_values = super().bulk_to_python(values)
            for i, val in enumerate(python_values):
                val['rendered_text'] = values[i]['rendered_text']
            return python_values
    

    and then that should hopefully make rendered_text available alongside markdown_text and some_other_property when accessing the block value on your template.