wagtail v4 new page issue
When trying to crate a new page in the example django app in the project the following error happens.
Seems to relate to the FormChooserBlock as when I comment out form = FormChooserBlock() (line #45 in blocks.py) the page renders.
Can't seem to see what the issue could be. But this might be the only blocker for supporting version 4?
To reproduce, edit the setup.py to require wagtail==4.0.* docker-compose up then try to add a child page to the default page.
app_1 | Internal Server Error: /cms/pages/add/example/basicpage/2/
app_1 | Traceback (most recent call last):
app_1 | File "/usr/local/lib/python3.8/site-packages/django/core/handlers/exception.py", line 55, in inner
app_1 | response = get_response(request)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/core/handlers/base.py", line 220, in _get_response
app_1 | response = response.render()
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/response.py", line 114, in render
app_1 | self.content = self.rendered_content
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/response.py", line 92, in rendered_content
app_1 | return template.render(context, self._request)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/backends/django.py", line 62, in render
app_1 | return self.template.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 175, in render
app_1 | return self._render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 167, in _render
app_1 | return self.nodelist.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 157, in render
app_1 | return compiled_parent._render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 167, in _render
app_1 | return self.nodelist.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 157, in render
app_1 | return compiled_parent._render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 167, in _render
app_1 | return self.nodelist.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 157, in render
app_1 | return compiled_parent._render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 167, in _render
app_1 | return self.nodelist.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 63, in render
app_1 | result = block.nodelist.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/loader_tags.py", line 63, in render
app_1 | result = block.nodelist.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1064, in render
app_1 | output = self.filter_expression.resolve(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 715, in resolve
app_1 | obj = self.var.resolve(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 847, in resolve
app_1 | value = self._resolve_lookup(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 914, in _resolve_lookup
app_1 | current = current()
app_1 | File "/usr/local/lib/python3.8/site-packages/wagtail/admin/panels.py", line 392, in render_form_content
app_1 | return mark_safe(self.render_html() + self.render_missing_fields())
app_1 | File "/usr/local/lib/python3.8/site-packages/wagtail/admin/ui/components.py", line 20, in render_html
app_1 | return template.render(context_data)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/backends/django.py", line 62, in render
app_1 | return self.template.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 175, in render
app_1 | return self._render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 167, in _render
app_1 | return self.nodelist.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/defaulttags.py", line 238, in render
app_1 | nodelist.append(node.render_annotated(context))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1064, in render
app_1 | output = self.filter_expression.resolve(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 715, in resolve
app_1 | obj = self.var.resolve(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 847, in resolve
app_1 | value = self._resolve_lookup(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 914, in _resolve_lookup
app_1 | current = current()
app_1 | File "/usr/local/lib/python3.8/site-packages/wagtail/admin/ui/components.py", line 20, in render_html
app_1 | return template.render(context_data)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/backends/django.py", line 62, in render
app_1 | return self.template.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 175, in render
app_1 | return self._render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 167, in _render
app_1 | return self.nodelist.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/defaulttags.py", line 238, in render
app_1 | nodelist.append(node.render_annotated(context))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/wagtail/admin/templatetags/wagtailadmin_tags.py", line 948, in render
app_1 | children = self.nodelist.render(context) if self.nodelist else ""
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
app_1 | return SafeString("".join([node.render_annotated(context) for node in self]))
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
app_1 | return self.render(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/template/library.py", line 237, in render
app_1 | output = self.func(*resolved_args, **resolved_kwargs)
app_1 | File "/usr/local/lib/python3.8/site-packages/wagtail/admin/templatetags/wagtailadmin_tags.py", line 876, in component
app_1 | return obj.render_html(context)
app_1 | File "/usr/local/lib/python3.8/site-packages/wagtail/admin/ui/components.py", line 15, in render_html
app_1 | context_data = self.get_context_data(parent_context)
app_1 | File "/usr/local/lib/python3.8/site-packages/wagtail/admin/panels.py", line 814, in get_context_data
app_1 | rendered_field = self.bound_field.as_widget(attrs=widget_attrs)
app_1 | File "/usr/local/lib/python3.8/site-packages/django/forms/boundfield.py", line 99, in as_widget
app_1 | return widget.render(
app_1 | File "/usr/local/lib/python3.8/site-packages/wagtail/blocks/base.py", line 552, in render
app_1 | return self.render_with_errors(
app_1 | File "/usr/local/lib/python3.8/site-packages/wagtail/blocks/base.py", line 546, in render_with_errors
app_1 | block_json=self.block_json,
app_1 | File "/usr/local/lib/python3.8/site-packages/wagtail/blocks/base.py", line 523, in block_json
app_1 | return self._block_json
app_1 | AttributeError: 'BlockWidget' object has no attribute '_block_json'
I'm having the same problem.
The Problem here is, that wagtail reworked the BaseChooser Widget. Wagtailstreamforms created a FormChooser which inherits wagtails BaseChooser and overwrites the inner widget, which does not work anymore. I was not able to figure out how get the FormChooser to run again under the new circumstances but figured we could just use a ChoiceBlock instead.
So here are the good news, at least for a hotfix inside your own Project. In my case we use wagtail as a headless CMS and consume the API to represent the data in an external Frontend Application. In that case you can simply overwrite the FormChooser like so with a little helper function like so:
from wagtail import blocks
from wagtailstreamforms.blocks import WagtailFormBlock
from wagtailstreamforms.models.form import Form
def getFormblockChoices():
choices = []
forms = Form.objects.all()
for form in forms:
choices.append((form.id, form.title))
return choices
class APIWagtailFormBlock(WagtailFormBlock):
FORMBLOCK_CHOICES = getFormblockChoices()
form = blocks.ChoiceBlock(choices=FORMBLOCK_CHOICES)
def get_api_representation(self, value, context=None):
if value:
return YourFormSerializer(context=context).to_representation(value)
else:
return None
Keep in mind that a ChoiceBlock returns a string, which makes it neccessary to overwrite the serializer as well, since it is expecting a model instance and not a string representation of a model. My ChoiceBlock returns the ID of the Form to grab the Instance we normally expect with the Object Manager like this:
from wagtailstreamforms.models.form import Form
form = Form.objects.get(id=obj['form'])
I bet that you could simply write a simple template tag that does exactly that if you are using wagtail the standard way to render your content.
Hope this helps at least for a quickfix and possibly with fixing the problem long term. Greetings, Phil.
Thanks, this helps a lot, I've already forked this project locally and got it working w/ v3, but wanting to move to v4. But we're not headless.
Haven't dived too deep, so excuse my ignorance, but that snippet is just for API, or will it also work for non-headless?
Hey @TonisPiip no worries,
It will work for your case as well, but as i already mentioned you probably need a custom template tag to deliver the form instance to your template.
I would try overwriting the formblock template (docs) since the var "form" referenced in the original template will only hold the form id and not the form instance. You could then define a template tag that finds your form instance and returns it to use that instance as a variable again.
I imagine the formblock template would look similar to this:
{% load custom_tags %}
{% find_form_instance form as form_instance %}
<h2>{{ value.form.title }}</h2>
<form{% if form.is_multipart %} enctype="multipart/form-data"{% endif %} action="{{ value.form_action }}" method="post" novalidate>
{{ form_instance.media }}
{% csrf_token %}
{% for hidden in form_instance.hidden_fields %}{{ hidden }}{% endfor %}
{% for field in form_instance.visible_fields %}
{% include 'streamforms/partials/form_field.html' %}
{% endfor %}
<input type="submit" value="{{ value.form.submit_button_text }}">
</form>
and the provided logic inside your custom_tags.py should look similar to:
from django import template
from wagtailstreamforms.models.form import Form
register = template.Library()
@register.simple_tag
def find_form_instance(form_id):
form_instance = None
if form_id:
form_instance = Form.objects.get(id=form_id)
return form_instance
Thank you very much for your suggestions. It's working for me on version 4 now. Rather than redefine the template (that I had already customized), I think it's better to override the render method: here's the original
ADD THIS to your models.py
from wagtailstreamforms.models.form import Form as StreamForm
class Version4WagtailFormBlock(WagtailFormBlock):
form = blocks.ChoiceBlock(choices=((form.id, form.title) for form in StreamForm.objects.all()))
def render(self, value, context=None):
# Substitute form (containing only the id) with the actual form
value['form'] = StreamForm.objects.filter(pk=value['form']).first()
form = value.get("form")
# check if we have a form, as they can be deleted, and we dont want to break the site with
# a none template value
if form:
self.meta.template = form.template_name
else:
self.meta.template = "streamforms/non_existent_form.html"
return super().render(value, context)
AND USE it like that in your StreamField
('form', Version4WagtailFormBlock(icon='tick-inverse', label="My awesome form")),
ah sweet the render method is a much better solution, thank you for the input :) also glad i could help.
As any time a form is added a new migration is wanted to be made....
from django.db import DatabaseError
from wagtail.blocks import ChoiceBlock
from wagtailstreamforms.blocks import WagtailFormBlock
from wagtailstreamforms.models.form import Form as StreamForm
class HackChoiceBlock(ChoiceBlock):
"Dont make new migations depending on DB state..."
def deconstruct(self):
_constructor_kwargs = self._constructor_kwargs
_constructor_kwargs["choices"] = []
return ("wagtail.blocks.ChoiceBlock", [], self._constructor_kwargs)
@staticmethod
def get_choices():
"""
Choices shouldn't access the db, or should be done lazily, however this is a hack in a hack
Main issue is that this db access is causing linting and publish to crash as it can't access the db
"""
try:
return tuple(StreamForm.objects.values_list("id", "title"))
except DatabaseError:
return []
class FormBlock(WagtailFormBlock):
"""
Wagatil v4 fix: https://github.com/labd/wagtailstreamforms/issues/200#issuecomment-1318933142"""
form = HackChoiceBlock(choices=HackChoiceBlock.get_choices())
def render(self, value, context=None):
# Substitute form (containing only the id) with the actual form
form = value["form"] = StreamForm.objects.filter(pk=value["form"]).first()
# check if we have a form, as they can be deleted, and we dont want to break the site with
# a none template value
if form:
self.meta.template = form.template_name
else:
self.meta.template = "streamforms/non_existent_form.html"
return super().render(value, context)
This is my hacky solution for not having db changes make django want to make a new migration, as well as having it possible for django to start w/o having a working db connection all the time.
This might be overkill, but when ran into the same problem on a Wagtail 4.1.1 upgrade I pulled in https://github.com/wagtail/wagtail-generic-chooser and used it to create a streamforms chooser...seems to work
In wagtail_hooks.py
from generic_chooser.views import ModelChooserViewSet
from generic_chooser.widgets import AdminChooser
from wagtailstreamforms.models import Form
class WagtailStreamFormsChooserViewSet(ModelChooserViewSet):
icon = 'form'
model = Form
page_title = 'Choose a form'
per_page = 10
class WagtailStreamFormsChooser(AdminChooser):
choose_one_text = 'Choose a form'
choose_another_text = 'Choose another form'
link_to_chosen_text = 'Edit this form'
model = Form
choose_modal_url_name = 'wagtailstreamforms_chooser:choose'
icon = 'form'
def get_edit_item_url(self, item):
# There may be a better way of getting the edit url
return f'/admin/wagtailstreamforms/form/edit/{item.pk}/'
@hooks.register('register_admin_viewset')
def register_wagtailstreamforms_chooser_viewset():
return WagtailStreamFormsChooserViewSet('wagtailstreamforms_chooser', url_prefix='wagtailstreamforms-chooser')
Then in your blocks.py
from django.utils.functional import cached_property
from wagtail import blocks
from wagtailstreamforms.blocks import WagtailFormBlock as _WagtailFormBlock
from wagtailstreamforms.models import Form
from .wagtail_hooks import WagtailStreamFormsChooser
class WagtailStreamFormsChooserBlock(blocks.ChooserBlock):
@cached_property
def target_model(self):
return Form
@cached_property
def widget(self):
return WagtailStreamFormsChooser()
def get_form_state(self, value):
return self.widget.get_value_data(value)
class WagtailFormBlock(_WagtailFormBlock):
form = WagtailStreamFormsChooserBlock()
@marts Thank you very much for the code. I will be using it in my project in a while.
Have you testing it with wagtail v4.2? I would hope it would work
@TonisPiip I've only tried it with Wagtail 4.1.1 - I try to stick with the Long Term Support releases generally, but I would have thought it will work fine on 4.2.
With the new release, 3.22, just should be fixed. Let me know if that version works for you @TonisPiip. Thank you very much @marts for your solution to this issue.
@VdeJong Thanks for the release, just tested with 4.2 and it worked. I can't confirm if the choices issue exists or not as I suppressed new migrations on StreamField changes.
There's still some RemovedInWagtail50Warning which should be fixed though, but that's not a breaking issue