wagtailstreamforms icon indicating copy to clipboard operation
wagtailstreamforms copied to clipboard

wagtail v4 new page issue

Open TonisPiip opened this issue 3 years ago • 1 comments

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'

TonisPiip avatar Sep 23 '22 11:09 TonisPiip

I'm having the same problem.

polesello avatar Oct 03 '22 22:10 polesello

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.

funkhaus-phil avatar Nov 17 '22 07:11 funkhaus-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?

TonisPiip avatar Nov 17 '22 11:11 TonisPiip

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

funkhaus-phil avatar Nov 17 '22 12:11 funkhaus-phil

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")),

polesello avatar Nov 17 '22 16:11 polesello

ah sweet the render method is a much better solution, thank you for the input :) also glad i could help.

funkhaus-phil avatar Nov 17 '22 17:11 funkhaus-phil

As any time a form is added a new migration is wanted to be made....

TonisPiip avatar Dec 20 '22 15:12 TonisPiip

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.

TonisPiip avatar Dec 20 '22 22:12 TonisPiip

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 avatar Jan 06 '23 14:01 marts

@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 avatar Jan 18 '23 21:01 TonisPiip

@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.

marts avatar Jan 18 '23 21:01 marts

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 avatar Mar 17 '23 12:03 VdeJong

@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

TonisPiip avatar Mar 21 '23 08:03 TonisPiip