Philipp S.
Philipp S.

Reputation: 981

Wagtail: How to filter text in streamfield within model.py

I would like to write a filter which replaces some $variables$ in my streamfield text. What is the best way to do this in my "Page" model? I tried the following but it is sometimes not working if I save my model as draft and publish it afterwards. Does anyone know a better way doing this?

class CityPage(Page, CityVariables):

    cityobject = models.ForeignKey(CityTranslated, on_delete=models.SET_NULL, null=True, blank=True)
    streamfield  = StreamField(BasicStreamBlock, null=True, blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('cityobject', classname="full"),
        StreamFieldPanel('streamfield'),

    ]

    def get_streamfield(self):
        for block in self.streamfield:
            if type(block.value) == unicode:
                block.value = self.replace_veriables(block.value)
            elif type(block.value) == RichText:
                block.value.source = self.replace_veriables(block.value.source)
            else:
                print "notimplemented"
        return self.streamfield

And this is just the class which replaces $variables$ with values from my database.

class CityVariables():

    def replace_veriables(self, repstr):
        reprules = self.get_city_context()
        for key, value in reprules.iteritems():
            repstr = repstr.replace(key, value)
        return repstr

    def get_city_context(self):
        context = {}
        if self.cityobject.population:
            context['$population$'] = unicode(self.cityobject.population)
        if self.cityobject.transregion:
            context['$region$'] = unicode(self.cityobject.transregion)
        return context


class BasicStreamBlock(blocks.StreamBlock):
    h2              = blocks.CharBlock(icon="title", classname="title")
    h3              = blocks.CharBlock(icon="title", classname="title")
    h4              = blocks.CharBlock(icon="title", classname="title")
    h5              = blocks.CharBlock(icon="title", classname="title")
    paragraph       = blocks.RichTextBlock(icon="pilcrow")
    image           = ImageChooserBlock(label="Image", icon="image")
    aligned_html    = blocks.RawHTMLBlock(icon="code", label='Raw HTML')

Upvotes: 0

Views: 1362

Answers (1)

LB Ben Johnston
LB Ben Johnston

Reputation: 5176

Here is a way to simply make the templated (converted) html output from streamfield from within your CityPage model.

Overview:

  • Use Python's built in basic Template system (or Python 3 docs), it is easy and will save you the hassle of dealing directly with the substitution.
  • Python's built in Template system uses $variablename not $variablename$ but works well and can be configured if really needed.
  • Avoid trying to build up the blocks within your streamfield manually, best to just to something like str(self.streamfield) which will force it to render into nice HTML.
  • Remember you can customise the html for any streamblock using class Meta: template = ... see docs.
  • Once we have our HTML output from the streamfield, we can use the string.Template class to create our output text by providing a dict of the template names and what to replace them with. Template variable names should not have the $ symbol in them (variablename not $variablename), the library takes care of that for you, it also takes care of basic string conversion.
  • For the sake of simplicity, I used a helpful model_to_dict util from Django to make the CityObject into a dict to pass directly to the template (think of this as your context for Django templates).
  • Note: This means that your $region would not work, it would need to match the fieldname eg. $transregion - or just change the fieldnames. It makes it easier for reading the code later if all the variables/fieldnames match anyway.
  • Before we can use this output in our final city_page.html template, we will need to mark it as safe for Django to render directly. Important: please be really careful about this as it means someone could save javascript code to the CityObject an it would run in the frontend, you may want another layer after model_to_dict to clear any potential js code.

Example: myapp/models.py

from django.forms.models import model_to_dict
from django.utils.safestring import mark_safe

from string import Template
# other imports... Page, etc


class CityPage(Page):

    cityobject = models.ForeignKey(
        CityTranslated, on_delete=models.SET_NULL, null=True, blank=True)
    streamfield = StreamField(BasicStreamBlock, null=True, blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('cityobject', classname="full"),
        StreamFieldPanel('streamfield'),
    ]

    def get_templated_streamfield(self):
        # using str is a quick way to force the content to be rendered
        rendered_streamfield = str(self.streamfield)
        # will generate a dict eg. {'population': 23000, 'transregion': 'EU'}
        # will not convert to values string/unicode - but this is handled by Template
        template_variables = model_to_dict(self.cityobject)
        template = Template(rendered_streamfield)
        # using safe_substitute will **not** throw an error if a variable exists without a value
        converted = template.safe_substitute(template_variables)
        # as we have html markup we must mark it as safe
        return mark_safe(converted)

Example: myapp/template/city_page.html

{% extends "base.html" %}
{% load wagtailimages_tags %}

{% block content %}
    {% include "base/include/header.html" %}
    <div class="container">
        <div class="row">
            <div class="col-md-6">
                <em>Streamfield Original (without templating)</em>
                {{ page.streamfield }}
            </div>
            <div class="col-md-2">
                <em>Streamfield with templating</em>
                {{ page.get_templated_streamfield }}
            </div>
        </div>
    </div>
{% endblock content %}

Upvotes: 2

Related Questions