Repository: wagtail/wagtail

Issue:
PageChooserPanel with foreign key linked to specific model: inconsistent behaviour
In the following example, `foo` is a page chooser field, where the developer has decided to limit the pages choosable to only those of a specific type rather than generic `wagtailcore.Page`. 

```
class MyPageThingy(Orderable):
    page = ParentalKey('core.MyPage', related_name='thingies')
    foo = models.ForeignKey('core.DifferentPage', related_name='+', null=True, blank=True,)

    panels = [
        PageChooserPanel('foo'),
    ]
```

I'd expect it to behave the same as the following:

```
class MyPageThingy(Orderable):
    page = ParentalKey('core.MyPage', related_name='thingies')
    foo = models.ForeignKey('wagtailcore.Page', related_name='+', null=True, blank=True,)

    panels = [
        PageChooserPanel('foo', 'core.DifferentPage'),
    ]
```

However while in the latter, the popup modal indicates that a restriction is in force, in the former no such indication is made and any page appears to be choosable.

In fact after selecting a "wrong" page then saving the changes, the user is given the comically poor message "Select a valid choice. That choice is not one of the available choices." 

Ideally the two versions above should behave the same, at least in terms of how the popup modal works.



Discussion:
I've run into the same problem, so I'm very glad to have found this ticket, as it led me to try the redundant approach of using:

``` python
class MyPageThingy(Orderable):
    page = ParentalKey('core.MyPage', related_name='thingies')
    foo = models.ForeignKey('core.DifferentPage', related_name='+', null=True, blank=True,)

    panels = [
        PageChooserPanel('foo', 'core.DifferentPage'),
    ]
```

This solved the issue of users being able to choose non-DifferentPage type content, even though the ForeignKey prevents that from being a viable choice. I'll see about making a pull request that fixes this, though I'm not entirely sure just yet that I'll be able to solve it.

I've discovered the culprit, but have no idea how to properly fix the problem. There's even a [TODO in the code about fixing this exact bug](https://github.com/torchbox/wagtail/blob/master/wagtail/wagtailadmin/edit_handlers.py#L476).

The issue that the building a `PageChooserPanel` doesn't tell the panel what page model it's attached to. Without access to the model class, the panel can't introspect the `ForeignKey` in order to determine the correct page type filter.

The best answer I could think of was to add a `base_model` argument to the `PageChooserPanel` constructor, a la `InlinePanel`. Unfortunately, this would break all existing code unless this new `base_model` argument were made optional. And what's the point of adding a new optional argument that serves the same purpose as the existing `page_type` argument?

Perhaps there's some way to inform the developer that using a more specific `ForeignKey` than `wagetailcore.Page` requires the use of the `page_type` argument on `PageChooserPanel`?

Hi @coredumperror - just to let you know I'm now working on a refactor of edit handlers so that they have access to the model class, as this is required by #823. This will allow us to solve this easily, and also to ditch the redundant `base_model` argument on `InlinePanel`.

Glad to hear it!

I have tested with latest master and the PageChooserPanel behaves correctly as expected in the first example. However, it seems that the changes break the page chooser -- it now displays as dropdown rather that the usual "Choose a page" button and modal. c542494 is a working commit. Consequent commits show the button (does nothing) and the dropdown. Latest master does away with the button alltogether.

Worth moving to a separate issue as the gist of this one has been fixed with #960.

Will look more into this tomorrow.

@zerolab Is this still failing for you on master? I've just tried various permutations of PageChooserPanel on wagtaildemo, and they're all rendering as expected (bearing in mind that the change proposed in this ticket has not yet been implemented):
- `PageChooserPanel('link_page')` where `link_page` is a foreign key to `wagtailcore.Page` (shows the chooser and allows choosing any page)
- `PageChooserPanel('link_page', 'demo.eventpage')` where `link_page` is still a foreign key to `wagtailcore.Page` (shows a chooser that only allows selecting pages of type "Event Page")
- `PageChooserPanel('link_page', 'demo.EventPage')` where `link_page` is still a foreign key to `wagtailcore.Page` (just in case capitalisation is significant - same as above)
- `PageChooserPanel('link_page')` where `link_page` is a foreign key to `demo.EventPage` (shows the chooser and allows choosing any page, aka the current broken behaviour)


Resolving patch:
diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py
--- a/wagtail/wagtailadmin/edit_handlers.py
+++ b/wagtail/wagtailadmin/edit_handlers.py
@@ -529,8 +529,8 @@ def target_content_type(cls):
 
                 cls._target_content_type = ContentType.objects.get_for_model(model)
             else:
-                # TODO: infer the content type by introspection on the foreign key
-                cls._target_content_type = ContentType.objects.get_by_natural_key('wagtailcore', 'page')
+                target_model = cls.model._meta.get_field(cls.field_name).rel.to
+                cls._target_content_type = ContentType.objects.get_for_model(target_model)
 
         return cls._target_content_type
 


Test patch:
diff --git a/wagtail/tests/migrations/0009_eventpagechoosermodel.py b/wagtail/tests/migrations/0009_eventpagechoosermodel.py
new file mode 100644
--- /dev/null
+++ b/wagtail/tests/migrations/0009_eventpagechoosermodel.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tests', '0008_registerdecorator'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='EventPageChooserModel',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('page', models.ForeignKey(help_text='more help text', to='tests.EventPage')),
+            ],
+            options={
+            },
+            bases=(models.Model,),
+        ),
+    ]
diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py
--- a/wagtail/tests/models.py
+++ b/wagtail/tests/models.py
@@ -504,6 +504,8 @@ class TaggedPage(Page):
 class PageChooserModel(models.Model):
     page = models.ForeignKey('wagtailcore.Page', help_text='help text')
 
+class EventPageChooserModel(models.Model):
+    page = models.ForeignKey('tests.EventPage', help_text='more help text')
 
 class SnippetChooserModel(models.Model):
     advert = models.ForeignKey(Advert, help_text='help text')
diff --git a/wagtail/wagtailadmin/tests/test_edit_handlers.py b/wagtail/wagtailadmin/tests/test_edit_handlers.py
--- a/wagtail/wagtailadmin/tests/test_edit_handlers.py
+++ b/wagtail/wagtailadmin/tests/test_edit_handlers.py
@@ -19,7 +19,7 @@
 from wagtail.wagtailadmin.widgets import AdminPageChooser, AdminDateInput
 from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
 from wagtail.wagtailcore.models import Page, Site
-from wagtail.tests.models import PageChooserModel, EventPage, EventPageSpeaker
+from wagtail.tests.models import PageChooserModel, EventPageChooserModel, EventPage, EventPageSpeaker
 from wagtail.tests.utils import WagtailTestUtils
 from wagtail.utils.deprecation import RemovedInWagtail11Warning
 
@@ -329,7 +329,7 @@ def setUp(self):
         model = PageChooserModel  # a model with a foreign key to Page which we want to render as a page chooser
 
         # a PageChooserPanel class that works on PageChooserModel's 'page' field
-        self.MyPageChooserPanel = PageChooserPanel('page', 'tests.EventPage').bind_to_model(PageChooserModel)
+        self.MyPageChooserPanel = PageChooserPanel('page').bind_to_model(PageChooserModel)
 
         # build a form class containing the fields that MyPageChooserPanel wants
         self.PageChooserForm = self.MyPageChooserPanel.get_form_class(PageChooserModel)
@@ -340,7 +340,6 @@ def setUp(self):
         self.test_instance = model.objects.create(page=self.christmas_page)
 
         self.form = self.PageChooserForm(instance=self.test_instance)
-        # self.form.errors['page'] = self.form.error_class(['errors'])  # FIXME: wat
         self.page_chooser_panel = self.MyPageChooserPanel(instance=self.test_instance,
                                                                 form=self.form)
 
@@ -350,7 +349,7 @@ def test_page_chooser_uses_correct_widget(self):
     def test_render_js_init(self):
         result = self.page_chooser_panel.render_as_field()
         expected_js = 'createPageChooser("{id}", "{model}", {parent});'.format(
-            id="id_page", model="tests.eventpage", parent=self.events_index_page.id)
+            id="id_page", model="wagtailcore.page", parent=self.events_index_page.id)
 
         self.assertIn(expected_js, result)
 
@@ -370,6 +369,34 @@ def test_render_error(self):
                                                                 form=form)
         self.assertIn('<span>This field is required.</span>', page_chooser_panel.render_as_field())
 
+    def test_override_page_type(self):
+        # Model has a foreign key to Page, but we specify EventPage in the PageChooserPanel
+        # to restrict the chooser to that page type
+        MyPageChooserPanel = PageChooserPanel('page', 'tests.EventPage').bind_to_model(EventPageChooserModel)
+        PageChooserForm = MyPageChooserPanel.get_form_class(EventPageChooserModel)
+        form = PageChooserForm(instance=self.test_instance)
+        page_chooser_panel = self.MyPageChooserPanel(instance=self.test_instance, form=form)
+
+        result = page_chooser_panel.render_as_field()
+        expected_js = 'createPageChooser("{id}", "{model}", {parent});'.format(
+            id="id_page", model="tests.eventpage", parent=self.events_index_page.id)
+
+        self.assertIn(expected_js, result)
+
+    def test_autodetect_page_type(self):
+        # Model has a foreign key to EventPage, which we want to autodetect
+        # instead of specifying the page type in PageChooserPanel
+        MyPageChooserPanel = PageChooserPanel('page').bind_to_model(EventPageChooserModel)
+        PageChooserForm = MyPageChooserPanel.get_form_class(EventPageChooserModel)
+        form = PageChooserForm(instance=self.test_instance)
+        page_chooser_panel = self.MyPageChooserPanel(instance=self.test_instance, form=form)
+
+        result = page_chooser_panel.render_as_field()
+        expected_js = 'createPageChooser("{id}", "{model}", {parent});'.format(
+            id="id_page", model="tests.eventpage", parent=self.events_index_page.id)
+
+        self.assertIn(expected_js, result)
+
     def test_target_content_type(self):
         result = PageChooserPanel(
             'barbecue',