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
Conversation
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): |
There was a problem hiding this comment.
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.
If we need an access to a request (to build a pagination object for a block, for example), we need to override the |
Other than questions above, looks good to me. Thank you @gasman! |
""" | ||
try: | ||
return self.render(value, context=context) | ||
except TypeError: |
There was a problem hiding this comment.
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})
There was a problem hiding this comment.
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 ;-)
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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 ;)
Hmm, good point! I can see how it would be useful to have access to the parent context inside |
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. |
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)
4216638
to
60851f4
Compare
Rebased, and updated to check the method signatures of |
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 |
@gasman I'm not sure that I more familiar with Jinja2 idioms, but I've used it for a long time.
{% with my_var=something %}
{% include 'my_template.html' %}
{% endwith %} It seems to me that implementation of a Jinja2 version of our I will back to this PR again in the morning. P.S.: Damn! We need another solution for Python 3.5 (see tests). |
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 :-) |
Adding an {% 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 |
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)
Merged in dbc4c9b (release notes in b79069b). I've created 2 issues:
Thanks @gasman and @timheap! |
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 thepage
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
andrender_basic
methods onBlock
now accept acontext
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.