Repository: wagtail/wagtail

Issue:
Allow specifying multiple page types in PageChooserPanel
Currently when adding links to pages it is possible to link to every page on the site. It would be great if the page links could be limited according to specified page types.



Discussion:
This can be done for a single page type, by passing a second parameter to PageChooserPanel:

```
PageChooserPanel('head_of_department', 'demo.PersonPage'),
```

Does this do what you need? (I'm aware we could really do with some more documentation here :-) )

Thank you, yes, that should do it.

Do you have any plans for it to allow multiple page types? Or would it work
with a superclass?

On 23 April 2014 14:44, Matt Westcott notifications@github.com wrote:

>  This can be done for a single page type, by passing a second parameter
> to PageChooserPanel:
> 
> PageChooserPanel('head_of_department', 'demo.PersonPage'),
> 
> Does this do what you need? (I'm aware we could really do with some more
> documentation here :-) )
> 
> —
> Reply to this email directly or view it on GitHubhttps://github.com/torchbox/wagtail/issues/199#issuecomment-41162071
> .

## 

José Miguel Vieira
Department of Digital Humanities
King's College London

Tel: +44 (0)20 7848 1242
http://www.kcl.ac.uk/artshums/depts/ddh/
http://emailcharter.org/

A superclass should work, I think. I dare say it wouldn't be too hard to add support for multiple types if there was a demand for that.

Ideally we'd also have it so that it picks up the appropriate page type automatically whenever the ForeignKey is pointing to something other than wagtailcore.Page.

If superclasses work, you can probably write a custom abstract base class similar to Python's `collections.Callable` and use it to implement a detailed filter.

If the superclass works it should cover most use cases.

It would be great if it could pick up the appropriate page type according to the ForeignKey.

Autodetecting page type from the ForeignKey is being addressed in #509. Will keep this ticket open for the other request, allowing multiple types to be specified. (I'm not 100% sure if the superclass approach works - in particular I suspect it might fail on abstract base classes, because we're using ContentTypes, and I don't think abstract classes have those. Either way, we can still improve things by having it accept an arbitrary list of page types.)

Would be nice to have this feature. 
I have different article types and from the section the admin can feature an article.

This affects me as well, I was trying to create a PageChooserPanel that filters by two page types. It would by useful if you could pass a list of page types to filter with.


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
@@ -544,44 +544,66 @@ def render_as_field(self):
 class BasePageChooserPanel(BaseChooserPanel):
     object_type_name = "page"
 
-    _target_content_type = None
+    _target_content_types = None
 
     @classmethod
     def widget_overrides(cls):
         return {cls.field_name: widgets.AdminPageChooser(
-            content_type=cls.target_content_type())}
+            content_types=cls.target_content_types())}
 
     @classmethod
-    def target_content_type(cls):
-        if cls._target_content_type is None:
-            if cls.page_type:
-                try:
-                    model = resolve_model_string(cls.page_type)
-                except LookupError:
-                    raise ImproperlyConfigured("{0}.page_type must be of the form 'app_label.model_name', given {1!r}".format(
-                        cls.__name__, cls.page_type))
-                except ValueError:
-                    raise ImproperlyConfigured("{0}.page_type refers to model {1!r} that has not been installed".format(
-                        cls.__name__, cls.page_type))
-
-                cls._target_content_type = ContentType.objects.get_for_model(model)
+    def target_content_types(cls):
+        if cls._target_content_types is None:
+            if cls.page_types:
+                models = []
+
+                for page_type in cls.page_types:
+                    try:
+                        models.append(resolve_model_string(page_type))
+                    except LookupError:
+                        raise ImproperlyConfigured("{0}.page_type must be of the form 'app_label.model_name', given {1!r}".format(
+                            cls.__name__, page_type))
+                    except ValueError:
+                        raise ImproperlyConfigured("{0}.page_type refers to model {1!r} that has not been installed".format(
+                            cls.__name__, page_type))
+
+                # Get a mapping of ContentType objects for the models
+                content_types = ContentType.objects.get_for_models(*models)
+
+                # Convert content types into a list and set it to _target_content_types
+                # As the content types were in a dict, the order would be randomised but
+                # we need the ordering on _target_content_types to be deterministic to
+                # simplify testing
+                cls._target_content_types = [
+                    content_types[model]
+                    for model in models
+                ]
             else:
-                target_model = cls.model._meta.get_field(cls.field_name).rel.to
-                cls._target_content_type = ContentType.objects.get_for_model(target_model)
+                # Find model by introspecting the ForeignKey
+                model = cls.model._meta.get_field(cls.field_name).rel.to
+                cls._target_content_types = [ContentType.objects.get_for_model(model)]
 
-        return cls._target_content_type
+        return cls._target_content_types
 
 
 class PageChooserPanel(object):
-    def __init__(self, field_name, page_type=None):
+    def __init__(self, field_name, page_types=None):
         self.field_name = field_name
-        self.page_type = page_type
+
+        # Make sure page_types is a list
+        if page_types is None:
+            page_types = []
+
+        if not isinstance(page_types, (list, tuple)):
+            page_types = [page_types]
+
+        self.page_types = page_types
 
     def bind_to_model(self, model):
         return type(str('_PageChooserPanel'), (BasePageChooserPanel,), {
             'model': model,
             'field_name': self.field_name,
-            'page_type': self.page_type,
+            'page_types': self.page_types,
         })
 
 
diff --git a/wagtail/wagtailadmin/views/chooser.py b/wagtail/wagtailadmin/views/chooser.py
--- a/wagtail/wagtailadmin/views/chooser.py
+++ b/wagtail/wagtailadmin/views/chooser.py
@@ -12,7 +12,7 @@
 
 def get_querystring(request):
     return urlencode({
-        'page_type': request.GET.get('page_type', ''),
+        'page_types': request.GET.get('page_types', ''),
         'allow_external_link': request.GET.get('allow_external_link', ''),
         'allow_email_link': request.GET.get('allow_email_link', ''),
         'prompt_for_link_text': request.GET.get('prompt_for_link_text', ''),
@@ -22,25 +22,29 @@ def get_querystring(request):
 def browse(request, parent_page_id=None):
     ITEMS_PER_PAGE = 25
 
-    page_type = request.GET.get('page_type') or 'wagtailcore.page'
-    content_type_app_name, content_type_model_name = page_type.split('.')
+    page_types = request.GET.get('page_types', 'wagtailcore.page').split(',')
 
-    try:
-        content_type = ContentType.objects.get_by_natural_key(content_type_app_name, content_type_model_name)
-    except ContentType.DoesNotExist:
-        raise Http404
-    desired_class = content_type.model_class()
+    desired_classes = []
+    for page_type in page_types:
+        content_type_app_name, content_type_model_name = page_type.split('.')
+
+        try:
+            content_type = ContentType.objects.get_by_natural_key(content_type_app_name, content_type_model_name)
+        except ContentType.DoesNotExist:
+            raise Http404
+
+        desired_classes.append(content_type.model_class())
 
     if parent_page_id:
         parent_page = get_object_or_404(Page, id=parent_page_id)
     else:
         parent_page = Page.get_first_root_node()
 
-    parent_page.can_choose = issubclass(parent_page.specific_class, desired_class)
+    parent_page.can_choose = issubclass(parent_page.specific_class, tuple(desired_classes))
     search_form = SearchForm()
     pages = parent_page.get_children()
 
-    if desired_class == Page:
+    if desired_classes == [Page]:
         # apply pagination first, since we know that the page listing won't
         # have to be filtered, and that saves us walking the entire list
         p = request.GET.get('p', 1)
@@ -63,7 +67,7 @@ def browse(request, parent_page_id=None):
 
         shown_pages = []
         for page in pages:
-            page.can_choose = issubclass(page.specific_class or Page, desired_class)
+            page.can_choose = issubclass(page.specific_class or Page, tuple(desired_classes))
             page.can_descend = page.get_children_count()
 
             if page.can_choose or page.can_descend:
@@ -86,29 +90,43 @@ def browse(request, parent_page_id=None):
         'parent_page': parent_page,
         'pages': pages,
         'search_form': search_form,
-        'page_type_string': page_type,
-        'page_type_name': desired_class.get_verbose_name(),
+        'page_type_string': ','.join(page_types),
+        'page_type_names': [desired_class.get_verbose_name() for desired_class in desired_classes],
         'page_types_restricted': (page_type != 'wagtailcore.page')
     })
 
 
 def search(request, parent_page_id=None):
-    page_type = request.GET.get('page_type') or 'wagtailcore.page'
-    content_type_app_name, content_type_model_name = page_type.split('.')
+    page_types = request.GET.get('page_types')
+    content_types = []
+
+    # Convert page_types string into list of ContentType objects
+    if page_types:
+        for page_type in page_types.split(','):
+            content_type_app_name, content_type_model_name = page_type.split('.')
 
-    try:
-        content_type = ContentType.objects.get_by_natural_key(content_type_app_name, content_type_model_name)
-    except ContentType.DoesNotExist:
-        raise Http404
-    desired_class = content_type.model_class()
+            try:
+                content_types.append(ContentType.objects.get_by_natural_key(content_type_app_name, content_type_model_name))
+            except ContentType.DoesNotExist:
+                raise Http404
 
     search_form = SearchForm(request.GET)
     if search_form.is_valid() and search_form.cleaned_data['q']:
-        pages = desired_class.objects.exclude(
+        pages = Page.objects.exclude(
             depth=1  # never include root
-        ).filter(title__icontains=search_form.cleaned_data['q'])[:10]
+        )
+
+        # Restrict content types
+        if content_types:
+            pages = pages.filter(content_type__in=content_types)
+
+        # Do search
+        pages = pages.filter(title__icontains=search_form.cleaned_data['q'])
+
+        # Truncate results
+        pages = pages[:10]
     else:
-        pages = desired_class.objects.none()
+        pages = Page.objects.none()
 
     shown_pages = []
     for page in pages:
diff --git a/wagtail/wagtailadmin/widgets.py b/wagtail/wagtailadmin/widgets.py
--- a/wagtail/wagtailadmin/widgets.py
+++ b/wagtail/wagtailadmin/widgets.py
@@ -110,17 +110,21 @@ def __init__(self, **kwargs):
 
 
 class AdminPageChooser(AdminChooser):
-    target_content_type = None
+    target_content_types = None
     choose_one_text = _('Choose a page')
     choose_another_text = _('Choose another page')
     link_to_chosen_text = _('Edit this page')
 
-    def __init__(self, content_type=None, **kwargs):
+    def __init__(self, content_types=None, **kwargs):
         super(AdminPageChooser, self).__init__(**kwargs)
-        self.target_content_type = content_type or ContentType.objects.get_for_model(Page)
+        self.target_content_types = content_types or [ContentType.objects.get_for_model(Page)]
 
     def render_html(self, name, value, attrs):
-        model_class = self.target_content_type.model_class()
+        if len(self.target_content_types) == 1:
+            model_class = self.target_content_types[0].model_class()
+        else:
+            model_class = Page
+
         instance, value = self.get_instance_and_id(model_class, value)
 
         original_field_html = super(AdminPageChooser, self).render_html(name, value, attrs)
@@ -134,17 +138,24 @@ def render_html(self, name, value, attrs):
         })
 
     def render_js_init(self, id_, name, value):
-        model_class = self.target_content_type.model_class()
-        if isinstance(value, model_class):
+        if isinstance(value, Page):
             page = value
         else:
+            if len(self.target_content_types) == 1:
+                model_class = self.target_content_types[0].model_class()
+            else:
+                model_class = Page
+
             page = self.get_instance(model_class, value)
+
         parent = page.get_parent() if page else None
-        content_type = self.target_content_type
 
         return "createPageChooser({id}, {content_type}, {parent});".format(
             id=json.dumps(id_),
-            content_type=json.dumps('{app}.{model}'.format(
-                app=content_type.app_label,
-                model=content_type.model)),
+            content_type=json.dumps([
+                '{app}.{model}'.format(
+                    app=content_type.app_label,
+                    model=content_type.model)
+                for content_type in self.target_content_types
+            ]),
             parent=json.dumps(parent.id if parent else None))


Test patch:
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
@@ -367,7 +367,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(
+        expected_js = 'createPageChooser("{id}", ["{model}"], {parent});'.format(
             id="id_page", model="wagtailcore.page", parent=self.events_index_page.id)
 
         self.assertIn(expected_js, result)
@@ -400,11 +400,25 @@ def test_override_page_type(self):
         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(
+        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_multiple_page_types(self):
+        # Model has a foreign key to Page, but we specify EventPage, and SimplePage in
+        # the PageChooserPanel to restrict the chooser to that page type
+        MyPageChooserPanel = PageChooserPanel('page', ['tests.EventPage', 'tests.SimplePage']).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}", ["{model1}", "{model2}"], {parent});'.format(
+            id="id_page", model1="tests.eventpage", model2="tests.simplepage", 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
@@ -414,33 +428,41 @@ def test_autodetect_page_type(self):
         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(
+        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):
+    def test_target_content_types(self):
+        result = PageChooserPanel(
+            'barbecue',
+            'tests.simplepage'
+        ).bind_to_model(PageChooserModel).target_content_types()
+        self.assertEqual(result[0].name, 'simple page')
+
+    def test_target_content_types_multiple_page_types(self):
         result = PageChooserPanel(
             'barbecue',
-            'wagtailcore.site'
-        ).bind_to_model(PageChooserModel).target_content_type()
-        self.assertEqual(result.name, 'Site')
+            ['tests.simplepage', 'tests.eventpage'],
+        ).bind_to_model(PageChooserModel).target_content_types()
+        self.assertEqual(result[0].name, 'simple page')
+        self.assertEqual(result[1].name, 'event page')
 
-    def test_target_content_type_malformed_type(self):
+    def test_target_content_types_malformed_type(self):
         result = PageChooserPanel(
             'barbecue',
             'snowman'
         ).bind_to_model(PageChooserModel)
         self.assertRaises(ImproperlyConfigured,
-                          result.target_content_type)
+                          result.target_content_types)
 
-    def test_target_content_type_nonexistent_type(self):
+    def test_target_content_types_nonexistent_type(self):
         result = PageChooserPanel(
             'barbecue',
             'snowman.lorry'
         ).bind_to_model(PageChooserModel)
         self.assertRaises(ImproperlyConfigured,
-                          result.target_content_type)
+                          result.target_content_types)
 
 
 class TestInlinePanel(TestCase, WagtailTestUtils):
diff --git a/wagtail/wagtailadmin/tests/test_page_chooser.py b/wagtail/wagtailadmin/tests/test_page_chooser.py
--- a/wagtail/wagtailadmin/tests/test_page_chooser.py
+++ b/wagtail/wagtailadmin/tests/test_page_chooser.py
@@ -41,7 +41,7 @@ def test_search_no_results(self):
         self.assertContains(response, "There are 0 matches")
 
     def test_get_invalid(self):
-        response = self.search({'page_type': 'foo.bar'})
+        response = self.search({'page_types': 'foo.bar'})
         self.assertEqual(response.status_code, 404)