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

wagtail v4 new page issue #200

Closed
TonisPiip opened this issue Sep 23, 2022 · 13 comments
Closed

wagtail v4 new page issue #200

TonisPiip opened this issue Sep 23, 2022 · 13 comments

Comments

@TonisPiip
Copy link

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'
@polesello
Copy link

I'm having the same problem.

@funkhaus-phil
Copy link

funkhaus-phil commented Nov 17, 2022

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.

@TonisPiip
Copy link
Author

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?

@funkhaus-phil
Copy link

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

@polesello
Copy link

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

@funkhaus-phil
Copy link

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

@TonisPiip
Copy link
Author

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

@TonisPiip
Copy link
Author

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.

@marts
Copy link

marts commented Jan 6, 2023

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()

@TonisPiip
Copy link
Author

@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

@marts
Copy link

marts commented Jan 18, 2023

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

VdeJong added a commit that referenced this issue Mar 8, 2023
Based on #200 (comment), wagtail_generic_chooser is introduced to solve #200. Also, wagtail 4.2 support is added and Django 4.1
VdeJong added a commit that referenced this issue Mar 17, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
* Update to version 3.22

Based on #200 (comment), wagtail_generic_chooser is introduced to solve #200. Also, wagtail 4.2 support is added and Django 4.1

* format files

* seperate -- from command arg

* set max version for wagtail-generic-chooser
@VdeJong
Copy link
Contributor

VdeJong commented Mar 17, 2023

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.

@TonisPiip
Copy link
Author

@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

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

No branches or pull requests

5 participants