Repository: wagtail/wagtail

Issue:
ModelAdmin forms that have Choosers inside MultiFieldPanels cause a crash
### Issue Summary

If you define a model or snippet, and that model's `panels` places a Chooser of any kind (ImageChooser, PageChooser, DocumentChooser) inside a `MultiFieldPanel`, attempting to access the form through the ModelAdmin interface causes the following error:

```
  ...
  File "/code/wagtail/wagtail/admin/edit_handlers.py", line 561, in get_chosen_item
    field = self.instance._meta.get_field(self.field_name)

Exception Type: AttributeError at /admin/base/simplephoto/create/
Exception Value: 'NoneType' object has no attribute '_meta'
```

This started happening after commit 80cc70b7c, which is after v2.10, so you need to be using Wagtail master to get this behavior (we found this bug because we need the Nested Collections code that was merged in a few commits later). If you roll Wagtail back to before that commit, or patch out the behavior changes in `ModelAdminForm` from that commit, no crash occurs and the forms work fine.

Oddly, the error does not occur if you access the same form through the Snippets interface.

### Steps to Reproduce

You can reproduce this error by adding the following two chunks of code to a stock bakerydemo (using the docker-wagtail-develop project):

Add this to the bottom bakerydemo/base/models.py:

```
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.documents.edit_handlers import DocumentChooserPanel

@register_snippet
class SimplePhoto(ClusterableModel):
    """
    This model contains the minimum set up to trigger the error:
    'NoneType' object has no attribute '_meta'
    when this model is used as a ModeAdmin rather than a Snippet.
    """

    image = models.ForeignKey(
        'wagtailimages.Image',
        on_delete=models.CASCADE,
        related_name='+'
    )
    live = models.BooleanField(
        verbose_name='Is Live',
        default=False,
    )

    panels = [
        MultiFieldPanel(
            [
                ImageChooserPanel('image'),
                FieldPanel('live'),
            ],
            heading= 'Simple Photo'
        ),
    ]

    # This code works, though, as it doesn't place a Chooser in a MultiFieldPanel:
    # panels = [
    #     MultiFieldPanel(
    #         [
    #             FieldPanel('live'),
    #         ],
    #         heading= 'Simple Photo'
    #     ),
    #     ImageChooserPanel('image'),
    # ]

@register_snippet
class RelatedLink(ClusterableModel):
    """
    Shows that this happens with page choosers, too.
    """

    title = models.CharField(
        max_length=256,
    )
    link = models.ForeignKey(
        HomePage,
        on_delete=models.CASCADE,
        related_name='+'
    )

    panels = [
        MultiFieldPanel(
            [
                FieldPanel('title'),
                PageChooserPanel('link'),
            ],
            heading= 'Related Link'
        ),
    ]


@register_snippet
class RelatedDocument(ClusterableModel):
    """
    Shows that Document choosers are also broken.
    """

    title = models.CharField(
        max_length=256,
    )
    doc = models.ForeignKey(
        'wagtaildocs.Document',
        on_delete=models.CASCADE,
        related_name='+'
    )

    panels = [
        MultiFieldPanel(
            [
                FieldPanel('title'),
                DocumentChooserPanel('doc'),
            ],
            heading='Document'
        ),
    ]


@register_snippet
class SimpleThing(ClusterableModel):
    """
    Shows that this no error occurs if there isn't a Chooser in a MultiFieldPanel.
    """

    title = models.CharField(
        max_length=256,
    )
    live = models.BooleanField(
        verbose_name='Is Live',
        default=False,
    )

    panels = [
        MultiFieldPanel(
            [
                FieldPanel('title'),
                FieldPanel('live'),
            ],
            heading= 'Simple Thing'
        ),
    ]
```

Add this to the bottom of bakerydemo/base/wagtail_hooks.py:
```
from .models import SimplePhoto, SimpleThing, RelatedLink, RelatedDocument

class SimplePhotoAdmin(ModelAdmin):
    model = SimplePhoto
    menu_label = "Simple Photos"
    menu_order = 130
    add_to_settings_menu = False
    exclude_from_explorer = True
    list_display = ('image', 'live')
    search_fields = ('image__title',)


class RelatedLinkAdmin(ModelAdmin):
    model = RelatedLink
    menu_label = "Related Links"
    menu_order = 135
    add_to_settings_menu = False
    exclude_from_explorer = True
    list_display = ('title', 'link')
    search_fields = ('title',)


class RelatedDocumentAdmin(ModelAdmin):
    model = RelatedDocument
    menu_label = "Related Documents"
    menu_order = 137
    add_to_settings_menu = False
    exclude_from_explorer = True
    list_display = ('title', 'link')
    search_fields = ('title',)


class SimpleThingAdmin(ModelAdmin):
    model = SimpleThing
    menu_label = "Simple Things"
    menu_order = 138
    add_to_settings_menu = False
    exclude_from_explorer = True
    list_display = ('title', 'live')
    search_fields = ('title',)


modeladmin_register(SimplePhotoAdmin)
modeladmin_register(RelatedLinkAdmin)
modeladmin_register(RelatedDocumentAdmin)
modeladmin_register(SimpleThingAdmin)
```

Then do `manage.py makemigrations` and `manage.py migrate`, and you'll be able to reproduce the crash.

Trying to add or edit a SimplePhoto, RelatedLink, or RelatedDocument via the ModelAdmin UI (the links directly in the sidebar) triggers the error. Trying to add any of these via the Snippets UI does not trigger the error. 

If you move the `XYZChooserPanel` out of the `MultiFieldPanel` and into the top level of the `panels` list (as seen in the part with the `This code works...` comment), the crash in the ModelAdmin UI goes away. So it's gotta be _something_ to do with MultiFieldPanels and Choosers, but I haven't got a clue what.

I wish I had the foggiest clue what needs to actually change in order to fix this crash, but I'm utterly stumped as to why it's happening at all.


Discussion:

Resolving patch:
diff --git a/wagtail/contrib/modeladmin/views.py b/wagtail/contrib/modeladmin/views.py
--- a/wagtail/contrib/modeladmin/views.py
+++ b/wagtail/contrib/modeladmin/views.py
@@ -118,10 +118,13 @@ def get_form(self):
         return form
 
     def get_edit_handler(self):
+        instance = self.get_instance()
         edit_handler = self.model_admin.get_edit_handler(
-            instance=self.get_instance(), request=self.request
+            instance=instance, request=self.request
+        )
+        return edit_handler.bind_to(
+            model=self.model_admin.model, request=self.request, instance=instance
         )
-        return edit_handler.bind_to(model=self.model_admin.model, request=self.request)
 
     def get_form_class(self):
         return self.edit_handler.get_form_class()


Test patch:
diff --git a/wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py b/wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py
--- a/wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py
+++ b/wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py
@@ -8,9 +8,10 @@
 
 from wagtail.admin.edit_handlers import FieldPanel, TabbedInterface
 from wagtail.contrib.modeladmin.helpers.search import DjangoORMSearchHandler
+from wagtail.core.models import Page
 from wagtail.images.models import Image
 from wagtail.images.tests.utils import get_test_image_file
-from wagtail.tests.modeladmintest.models import Author, Book, Publisher, Token
+from wagtail.tests.modeladmintest.models import Author, Book, Publisher, RelatedLink, Token
 from wagtail.tests.modeladmintest.wagtail_hooks import BookModelAdmin
 from wagtail.tests.utils import WagtailTestUtils
 
@@ -272,6 +273,21 @@ def test_clean_form_once(self):
 
             mock_form_clean.assert_called_once()
 
+    def test_create_view_with_multifieldpanel(self):
+        # https://github.com/wagtail/wagtail/issues/6413
+        response = self.client.get('/admin/modeladmintest/relatedlink/create/')
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post('/admin/modeladmintest/relatedlink/create/', {
+            'title': "Homepage",
+            'link': Page.objects.filter(depth=2).first().id,
+        })
+        # Should redirect back to index
+        self.assertRedirects(response, '/admin/modeladmintest/relatedlink/')
+
+        # Check that the link was created
+        self.assertEqual(RelatedLink.objects.filter(title="Homepage").count(), 1)
+
 
 class TestInspectView(TestCase, WagtailTestUtils):
     fixtures = ['modeladmintest_test.json']
@@ -409,6 +425,22 @@ def test_clean_form_once(self):
 
             mock_form_clean.assert_called_once()
 
+    def test_edit_view_with_multifieldpanel(self):
+        # https://github.com/wagtail/wagtail/issues/6413
+        link = RelatedLink.objects.create(title='Homepage', link=Page.objects.filter(depth=2).first())
+        response = self.client.get('/admin/modeladmintest/relatedlink/edit/%d/' % link.id)
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post('/admin/modeladmintest/relatedlink/edit/%d/' % link.id, {
+            'title': "Homepage edited",
+            'link': Page.objects.filter(depth=2).first().id,
+        })
+        # Should redirect back to index
+        self.assertRedirects(response, '/admin/modeladmintest/relatedlink/')
+
+        # Check that the link was updated
+        self.assertEqual(RelatedLink.objects.filter(title="Homepage edited").count(), 1)
+
 
 class TestPageSpecificViews(TestCase, WagtailTestUtils):
     fixtures = ['modeladmintest_test.json']
diff --git a/wagtail/tests/modeladmintest/migrations/0009_relatedlink.py b/wagtail/tests/modeladmintest/migrations/0009_relatedlink.py
new file mode 100644
--- /dev/null
+++ b/wagtail/tests/modeladmintest/migrations/0009_relatedlink.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.1 on 2020-10-01 18:16
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wagtailcore', '0057_page_locale_fields_notnull'),
+        ('modeladmintest', '0008_solobook'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='RelatedLink',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(max_length=255)),
+                ('link', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.page')),
+            ],
+        ),
+    ]
diff --git a/wagtail/tests/modeladmintest/models.py b/wagtail/tests/modeladmintest/models.py
--- a/wagtail/tests/modeladmintest/models.py
+++ b/wagtail/tests/modeladmintest/models.py
@@ -1,6 +1,8 @@
 from django.db import models
 
-from wagtail.admin.edit_handlers import FieldPanel, ObjectList, TabbedInterface
+from wagtail.admin.edit_handlers import (
+    FieldPanel, MultiFieldPanel, ObjectList, PageChooserPanel, TabbedInterface
+)
 from wagtail.core.models import Page
 from wagtail.search import index
 
@@ -124,3 +126,24 @@ class Friend(models.Model):
 
     def __str__(self):
         return self.first_name
+
+
+class RelatedLink(models.Model):
+    title = models.CharField(
+        max_length=255,
+    )
+    link = models.ForeignKey(
+        Page,
+        on_delete=models.CASCADE,
+        related_name='+'
+    )
+
+    panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel('title'),
+                PageChooserPanel('link'),
+            ],
+            heading='Related Link'
+        ),
+    ]
diff --git a/wagtail/tests/modeladmintest/wagtail_hooks.py b/wagtail/tests/modeladmintest/wagtail_hooks.py
--- a/wagtail/tests/modeladmintest/wagtail_hooks.py
+++ b/wagtail/tests/modeladmintest/wagtail_hooks.py
@@ -6,7 +6,9 @@
 from wagtail.tests.testapp.models import BusinessChild, EventPage, SingleEventPage
 
 from .forms import PublisherModelAdminForm
-from .models import Author, Book, Contributor, Friend, Person, Publisher, Token, VenuePage, Visitor
+from .models import (
+    Author, Book, Contributor, Friend, Person, Publisher, RelatedLink, Token, VenuePage, Visitor
+)
 
 
 class AuthorModelAdmin(ModelAdmin):
@@ -164,6 +166,11 @@ class BusinessChildAdmin(ModelAdmin):
     menu_label = "BusinessSprog"
 
 
+class RelatedLinkAdmin(ModelAdmin):
+    model = RelatedLink
+    menu_label = "Related Links"
+
+
 modeladmin_register(AuthorModelAdmin)
 modeladmin_register(BookModelAdmin)
 modeladmin_register(TokenModelAdmin)
@@ -174,3 +181,4 @@ class BusinessChildAdmin(ModelAdmin):
 modeladmin_register(FriendAdmin)
 modeladmin_register(VisitorAdmin)
 modeladmin_register(ContributorAdmin)
+modeladmin_register(RelatedLinkAdmin)