Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

include_block template tag for improved StreamField template inclusion #2786

Closed
wants to merge 12 commits into from

Conversation

gasman
Copy link
Collaborator

@gasman gasman commented Jun 27, 2016

As proposed in #1743 (comment) - this PR implements an {% include_block my_block %} template tag, which works equivalently to Django's {% include 'my_template.html' %} tag (including constructs like {% include_block my_block with classname='foo' only %}).

This improves on the current convention of rendering StreamField blocks using variable tags e.g. {{ my_block }}, as it allows the child template to inherit the template context - this is necessary for the {% pageurl %} tag to work (and also provides access to the page variable, which is frequently requested - e.g. to allow building StreamField-based forms that post back to the current page).

As part of this change, the render and render_basic methods on Block now accept a context kwarg, which introduces a backwards incompatibility (since third-party libraries may have overridden these methods with the old method signature) - I've put compatibility shims in place to mitigate this.

Fixes #1743, #2469.

@m1kola
Copy link
Contributor

m1kola commented Jul 4, 2016

At first glance looks good, but I want come back to it with fresh eyes tomorrow.

Thanks Matthew!



@register.tag
def include_block(parser, token):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that we need to add a similar tag for the Jinja2 template engine.

@m1kola
Copy link
Contributor

m1kola commented Jul 5, 2016

If we need an access to a request (to build a pagination object for a block, for example), we need to override the render method instead of creating the pagination object in the get_context method. Is that right?

@m1kola
Copy link
Contributor

m1kola commented Jul 5, 2016

Other than questions above, looks good to me.

Thank you @gasman!

"""
try:
return self.render(value, context=context)
except TypeError:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. If 3-rd party block raises TypeError (I don't know why they may do that :)), render method will be called twice even if block have proper signature. It will be hard to debug for 3-rd party developers.

    def render(self, value, context=None):
        # do something that raises a TypeError

Probably we need to use inspect. Something like this:

import inspect


class Foo:
    def render(self, value):
        print("I'm alive")
    def _render_with_context(self, value, context=None):
        if 'context' in inspect.getargspec(self.render).args:
            return self.render(value, context=context)
        else:
            result = self.render(value)
            # Warn and bla-bla-bla


test = Foo()
test._render_with_context(1, {"two": 2})

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's worth making our code more complicated (and less Pythonic...) just to handle this very obscure special case.

This would only be a problem if the render method has side effects - otherwise, it doesn't matter that we call it twice. A render method that throws TypeError and has side effects would have to be doing something very hacky and non-standard indeed - if they have a hard time debugging it as a result, I think that's fair punishment ;-)

Copy link
Contributor

@m1kola m1kola Jul 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to handle this very obscure special case.

It may be just typo or "4-th party code" can riseTypeError... For example, I've tried to add a pagination to a block. It's a bad solution because I pass request object into my block through templates, but it's a good example when it will be hard to figure out what's going on.

    def render(self, value, context=None):
        request = context.get('request') if context else None
        if not request:
            raise Exception('Sad trombone sound')

        pages = context.get('pages')

        per_page = settings.DEFAULT_PER_PAGE
        paginator = Paginator(pages, per_page)
        page_number = request.GET.get('page')

        try:
            pages = paginator.page(page_number)
        except PageNotAnInteger:
            # Hint: TypeError will be raised somewhere in Django's Paginator
            pages = paginator.page(1)
        except EmptyPage:
            pages = paginator.page(paginator.num_pages)

        context = dict(context)
        context['pages'] = pages

        return super().render(value, context)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, I see. So, the problem isn't really about render being unexpectedly called twice - it's that it's missing the context on the second occasion (and that's the one you'll see in the debug output). That's a fair point - I'll fix that with your suggested inspect approach. Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I haven't expected to see Exception: Sad trombone sound and it took some time to figure out why I've got this exception instead of TypeError that was thrown by the paginator.

inspect - it's just the first idea that came to my head. If you know more Pythonic way, feel free to use it ;)

@gasman
Copy link
Collaborator Author

gasman commented Jul 5, 2016

If we need an access to a request (to build a pagination object for a block, for example), we need to override the render method instead of creating the pagination object in the get_context method. Is that right?

Hmm, good point! I can see how it would be useful to have access to the parent context inside get_context. I think that's better handled in a separate PR though, as it's quite a bit more complicated than updating render / render_basic - we can't just change the signature to get_context(self, value, context=context) because we also need to consider what happens when old code calls super(MyBlock, self).get_context(value). (Writing the documentation to explain what it means to pass a context to get_context is going to be quite difficult too!)

@m1kola
Copy link
Contributor

m1kola commented Jul 5, 2016

I think that's better handled in a separate PR though

Absolutely agree.

Also you can implement a Jinja2 template tag in a separate PR, if you wish. But I think that we should release Jinja2 template tag in the same time as Django's one.

gasman added 10 commits July 5, 2016 17:21
Collectively, these are the objects encountered during template rendering which typically render
a block template when output inside {{ ... }} tags. Implementing render_as_block allows us to do
the same thing, but passing a template context as well.
Any bits of StreamField infrastructure that attempt to call render or render_basic
on a block with a 'context' kwarg, should (for now) also work on blocks that don't
accept the context kwarg, but output a RemovedInWagtail18Warning.
… kwarg

This avoids unexpected behaviour when the method legitimately accepts a context
kwarg, but happens to throw an unrelated TypeError - in this situation, the final
output (or error diagnostics) will behave as if the context was never passed,
making debugging difficult. See wagtail#2786 (comment)
@gasman
Copy link
Collaborator Author

gasman commented Jul 5, 2016

Rebased, and updated to check the method signatures of render and render_basic using inspect.getargspec, as suggested.

@gasman
Copy link
Collaborator Author

gasman commented Jul 5, 2016

I agree that we ought to support Jinja2 templates here too - I was meaning to mention that in the ticket description, but forgot...

I've just been reading the docs for Jinja2's include / import tags, and my first impression is that they differ significantly from Django's approach. Given that {% include_block %} is designed to closely follow the syntax of {% include %}, I feel that it would probably be better implemented by someone who's more familiar with Jinja2 idioms, and has a clear idea of what a Jinja2 version of {% include_block %} should look like. (@timheap, would that person be you? :-) )

@m1kola
Copy link
Contributor

m1kola commented Jul 5, 2016

@gasman I'm not sure that I more familiar with Jinja2 idioms, but I've used it for a long time.

include tags of both template engines are not so different. The only significant difference, in my opinion, is how Jinja2 allows you to control which variables will be passed into the context. You can specify with context (by default) or without context in include tag. If you want to pass only specified variables, you have to use with tag. It will be something like this:

{% with my_var=something %}
    {% include 'my_template.html' %}
{% endwith %}

It seems to me that implementation of a Jinja2 version of our include_block will be simpler than implementation of a Django version because with tag does almost all business. As far as I remember, with goes out of the box with Jinja2, but it disabled by default (btw, I don't think that we need to care if it disabled).

I will back to this PR again in the morning.

P.S.: Damn! We need another solution for Python 3.5 (see tests).

@gasman
Copy link
Collaborator Author

gasman commented Jul 5, 2016

Fix for Python 3.5 added. I'm afraid it's not pretty... I look forward to Wagtail 1.8, when we can remove this pile of hacks :-)

@mx-moth
Copy link
Contributor

mx-moth commented Jul 6, 2016

Adding an {% include_block %} tag should be possible. I think it would only need to support the with/without context, and would not have to support naming templates or extra kwargs:

{% include_block block %}{# Includes context by default #}
{% include_block block with context %}{# Explicitly include context #}
{% include_block block without context %}{# Does not use context #}

Template authors can use {% with %} or `{% set %} if they need to provide extra context.

@gasman gasman removed their assignment Jul 6, 2016
m1kola pushed a commit that referenced this pull request Jul 6, 2016
Update render and render_basic methods on Block to take a context kwarg

Update TableBlock to support passing extra context to render

Implement render_as_block on BoundBlock, StreamValue and StructValue.

Collectively, these are the objects encountered during template rendering which typically render
a block template when output inside {{ ... }} tags. Implementing render_as_block allows us to do
the same thing, but passing a template context as well.

Implement include_block tag

Support extra context vars on include_block via 'with foo=bar'

Support 'only' flag on include_block tag, to omit the parent context

Update StreamField documentation to cover the include_block tag

Rewrite 'BoundBlocks and values' docs based on the include_block tag

Add tests for blocks with legacy render / render_basic methods

Any bits of StreamField infrastructure that attempt to call render or render_basic
on a block with a 'context' kwarg, should (for now) also work on blocks that don't
accept the context kwarg, but output a RemovedInWagtail18Warning.

Explicitly test whether render / render_basic will accept a 'context' kwarg

This avoids unexpected behaviour when the method legitimately accepts a context
kwarg, but happens to throw an unrelated TypeError - in this situation, the final
output (or error diagnostics) will behave as if the context was never passed,
making debugging difficult. See #2786 (comment)
m1kola added a commit that referenced this pull request Jul 6, 2016
@m1kola
Copy link
Contributor

m1kola commented Jul 6, 2016

Merged in dbc4c9b (release notes in b79069b).

I've created 2 issues:

Thanks @gasman and @timheap!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants