From eb47526888ff8d92daaf023f81bce6e3b86607a8 Mon Sep 17 00:00:00 2001
From: Mikalai Radchuk <radchuk.m@gmail.com>
Date: Tue, 24 Jan 2017 20:03:21 +0300
Subject: [PATCH] Enhancement for PageChooserBlock

Allow the target_model argument to be a list or tuple.
---
 CHANGELOG.txt                             |  1 +
 docs/releases/1.10.rst                    |  1 +
 docs/topics/streamfield.rst               |  2 +-
 wagtail/wagtailcore/blocks/field_block.py | 49 ++++++++++++++++---
 wagtail/wagtailcore/tests/test_blocks.py  | 58 +++++++++++++++++++++--
 5 files changed, 100 insertions(+), 11 deletions(-)

diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index afb5bdfdb8..316f3b8403 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -10,6 +10,7 @@ Changelog
  * Added `construct_image_chooser_queryset`, `construct_document_chooser_queryset` and `construct_page_chooser_queryset` hooks (Gagaro)
  * The homepage created in the project template is now titled "Home" rather than "Homepage" (Karl Hobley)
  * Signal receivers for custom `Image` and `Rendition` models are connected automatically (Mike Dingjan)
+ * `PageChooserBlock` can now accept a list/tuple of page models as `target_model` (Mikalai Radchuk)
  * Fix: Marked 'Date from' / 'Date to' strings in wagtailforms for translation (Vorlif)
  * Fix: Unreliable preview is now reliable by always opening in a new window (Kjartan Sverrisson)
  * Fix: Fixed placement of `{{ block.super }}` in `snippets/type_index.html` (LB (Ben Johnston))
diff --git a/docs/releases/1.10.rst b/docs/releases/1.10.rst
index 47eeca369a..5be8b77560 100644
--- a/docs/releases/1.10.rst
+++ b/docs/releases/1.10.rst
@@ -20,6 +20,7 @@ Other features
  * Added ``construct_image_chooser_queryset``, ``construct_document_chooser_queryset`` and ``construct_page_chooser_queryset`` hooks (Gagaro)
  * The homepage created in the project template is now titled "Home" rather than "Homepage" (Karl Hobley)
  * Signal receivers for custom ``Image`` and ``Rendition`` models are connected automatically (Mike Dingjan)
+ * ``PageChooserBlock`` can now accept a list/tuple of page models as ``target_model`` (Mikalai Radchuk)
 
 
 Bug fixes
diff --git a/docs/topics/streamfield.rst b/docs/topics/streamfield.rst
index 51f50fc4c6..412627ce7c 100644
--- a/docs/topics/streamfield.rst
+++ b/docs/topics/streamfield.rst
@@ -251,7 +251,7 @@ A control for selecting a page object, using Wagtail's page browser. The followi
   If true, the field cannot be left blank.
 
 ``target_model`` (default: Page)
-  Restrict choices to a single Page type.
+  Restrict choices to one or more specific page types. Accepts a page model class, model name (as a string), or a list or tuple of these.
 
 ``can_choose_root`` (default: False)
   If true, the editor can choose the tree root as a page. Normally this would be undesirable, since the tree root is never a usable page, but in some specialised cases it may be appropriate. For example, a block providing a feed of related articles could use a PageChooserBlock to select which subsection of the site articles will be taken from, with the root corresponding to 'everywhere'.
diff --git a/wagtail/wagtailcore/blocks/field_block.py b/wagtail/wagtailcore/blocks/field_block.py
index d4afbf4c2e..435cd7a130 100644
--- a/wagtail/wagtailcore/blocks/field_block.py
+++ b/wagtail/wagtailcore/blocks/field_block.py
@@ -581,20 +581,48 @@ class ChooserBlock(FieldBlock):
 
 class PageChooserBlock(ChooserBlock):
 
-    def __init__(self, target_model='wagtailcore.Page', can_choose_root=False,
+    # TODO: rename target_model to page_type
+    def __init__(self, target_model=None, can_choose_root=False,
                  **kwargs):
-        self._target_model = target_model
+        if target_model:
+            # Convert single string/model into a list
+            if not isinstance(target_model, (list, tuple)):
+                target_model = [target_model]
+        else:
+            target_model = []
+
+        self._target_models = target_model
         self.can_choose_root = can_choose_root
         super(PageChooserBlock, self).__init__(**kwargs)
 
     @cached_property
     def target_model(self):
-        return resolve_model_string(self._target_model)
+        """
+        Defines the model used by the base ChooserBlock for ID <-> instance
+        conversions. If a single page type is specified in target_model,
+        we can use that to get the more specific instance "for free"; otherwise
+        use the generic Page model.
+        """
+        if len(self.target_models) == 1:
+            return self.target_models[0]
+
+        return resolve_model_string('wagtailcore.Page')
+
+    @cached_property
+    def target_models(self):
+        target_models = []
+
+        for target_model in self._target_models:
+            target_models.append(
+                resolve_model_string(target_model)
+            )
+
+        return target_models
 
     @cached_property
     def widget(self):
         from wagtail.wagtailadmin.widgets import AdminPageChooser
-        return AdminPageChooser(target_models=[self.target_model],
+        return AdminPageChooser(target_models=self.target_models,
                                 can_choose_root=self.can_choose_root)
 
     def render_basic(self, value, context=None):
@@ -605,9 +633,18 @@ class PageChooserBlock(ChooserBlock):
 
     def deconstruct(self):
         name, args, kwargs = super(PageChooserBlock, self).deconstruct()
+
         if 'target_model' in kwargs:
-            opts = self.target_model._meta
-            kwargs['target_model'] = '{}.{}'.format(opts.app_label, opts.object_name)
+            target_models = []
+
+            for target_model in self.target_models:
+                opts = target_model._meta
+                target_models.append(
+                    '{}.{}'.format(opts.app_label, opts.object_name)
+                )
+
+            kwargs['target_model'] = target_models
+
         return name, args, kwargs
 
     class Meta:
diff --git a/wagtail/wagtailcore/tests/test_blocks.py b/wagtail/wagtailcore/tests/test_blocks.py
index 69f3831f32..b27c06b487 100644
--- a/wagtail/wagtailcore/tests/test_blocks.py
+++ b/wagtail/wagtailcore/tests/test_blocks.py
@@ -21,7 +21,7 @@ from django.utils.translation import ugettext_lazy as __
 
 from wagtail.tests.testapp.blocks import LinkBlock as CustomLinkBlock
 from wagtail.tests.testapp.blocks import SectionBlock
-from wagtail.tests.testapp.models import SimplePage
+from wagtail.tests.testapp.models import EventPage, SimplePage
 from wagtail.utils.deprecation import RemovedInWagtail111Warning
 from wagtail.wagtailcore import blocks
 from wagtail.wagtailcore.models import Page
@@ -2265,11 +2265,31 @@ class TestPageChooserBlock(TestCase):
         self.assertIn(expected_html, christmas_form_html)
         self.assertIn("pick a page, any page", christmas_form_html)
 
-    def test_form_render_with_target_model(self):
+    def test_form_render_with_target_model_default(self):
+        block = blocks.PageChooserBlock()
+        empty_form_html = block.render_form(None, 'page')
+        self.assertIn('createPageChooser("page", ["wagtailcore.page"], null, false);', empty_form_html)
+
+    def test_form_render_with_target_model_string(self):
         block = blocks.PageChooserBlock(help_text="pick a page, any page", target_model='tests.SimplePage')
         empty_form_html = block.render_form(None, 'page')
         self.assertIn('createPageChooser("page", ["tests.simplepage"], null, false);', empty_form_html)
 
+    def test_form_render_with_target_model_literal(self):
+        block = blocks.PageChooserBlock(help_text="pick a page, any page", target_model=SimplePage)
+        empty_form_html = block.render_form(None, 'page')
+        self.assertIn('createPageChooser("page", ["tests.simplepage"], null, false);', empty_form_html)
+
+    def test_form_render_with_target_model_multiple_strings(self):
+        block = blocks.PageChooserBlock(help_text="pick a page, any page", target_model=['tests.SimplePage', 'tests.EventPage'])
+        empty_form_html = block.render_form(None, 'page')
+        self.assertIn('createPageChooser("page", ["tests.simplepage", "tests.eventpage"], null, false);', empty_form_html)
+
+    def test_form_render_with_target_model_multiple_literals(self):
+        block = blocks.PageChooserBlock(help_text="pick a page, any page", target_model=[SimplePage, EventPage])
+        empty_form_html = block.render_form(None, 'page')
+        self.assertIn('createPageChooser("page", ["tests.simplepage", "tests.eventpage"], null, false);', empty_form_html)
+
     def test_form_render_with_can_choose_root(self):
         block = blocks.PageChooserBlock(help_text="pick a page, any page", can_choose_root=True)
         empty_form_html = block.render_form(None, 'page')
@@ -2297,6 +2317,10 @@ class TestPageChooserBlock(TestCase):
         self.assertEqual(nonrequired_block.clean(christmas_page), christmas_page)
         self.assertEqual(nonrequired_block.clean(None), None)
 
+    def test_target_model_default(self):
+        block = blocks.PageChooserBlock()
+        self.assertEqual(block.target_model, Page)
+
     def test_target_model_string(self):
         block = blocks.PageChooserBlock(target_model='tests.SimplePage')
         self.assertEqual(block.target_model, SimplePage)
@@ -2305,17 +2329,43 @@ class TestPageChooserBlock(TestCase):
         block = blocks.PageChooserBlock(target_model=SimplePage)
         self.assertEqual(block.target_model, SimplePage)
 
+    def test_target_model_multiple_strings(self):
+        block = blocks.PageChooserBlock(target_model=['tests.SimplePage', 'tests.EventPage'])
+        self.assertEqual(block.target_model, Page)
+
+    def test_target_model_multiple_literals(self):
+        block = blocks.PageChooserBlock(target_model=[SimplePage, EventPage])
+        self.assertEqual(block.target_model, Page)
+
+    def test_deconstruct_target_model_default(self):
+        block = blocks.PageChooserBlock()
+        self.assertEqual(block.deconstruct(), (
+            'wagtail.wagtailcore.blocks.PageChooserBlock',
+            (), {}))
+
     def test_deconstruct_target_model_string(self):
         block = blocks.PageChooserBlock(target_model='tests.SimplePage')
         self.assertEqual(block.deconstruct(), (
             'wagtail.wagtailcore.blocks.PageChooserBlock',
-            (), {'target_model': 'tests.SimplePage'}))
+            (), {'target_model': ['tests.SimplePage']}))
 
     def test_deconstruct_target_model_literal(self):
         block = blocks.PageChooserBlock(target_model=SimplePage)
         self.assertEqual(block.deconstruct(), (
             'wagtail.wagtailcore.blocks.PageChooserBlock',
-            (), {'target_model': 'tests.SimplePage'}))
+            (), {'target_model': ['tests.SimplePage']}))
+
+    def test_deconstruct_target_model_multiple_strings(self):
+        block = blocks.PageChooserBlock(target_model=['tests.SimplePage', 'tests.EventPage'])
+        self.assertEqual(block.deconstruct(), (
+            'wagtail.wagtailcore.blocks.PageChooserBlock',
+            (), {'target_model': ['tests.SimplePage', 'tests.EventPage']}))
+
+    def test_deconstruct_target_model_multiple_literals(self):
+        block = blocks.PageChooserBlock(target_model=[SimplePage, EventPage])
+        self.assertEqual(block.deconstruct(), (
+            'wagtail.wagtailcore.blocks.PageChooserBlock',
+            (), {'target_model': ['tests.SimplePage', 'tests.EventPage']}))
 
 
 class TestStaticBlock(unittest.TestCase):