Merge branch 'kaedroho-multiple-page-types-in-page-chooser3'

Conflicts:
	CHANGELOG.txt
	docs/releases/1.1.rst
	wagtail/wagtailadmin/views/chooser.py
This commit is contained in:
Matt Westcott 2015-08-25 16:20:32 +01:00
commit f65d91ad01
11 changed files with 209 additions and 46 deletions

View file

@ -23,6 +23,7 @@ Changelog
* The `update_index` task now indexes objects in batches of 1000, to indicate progress and avoid excessive memory use
* Added database indexes on PageRevision and Image to improve performance on large sites
* Search in page chooser now uses Wagtail's search framework, to order results by relevance
* `PageChooserPanel` now supports passing a list (or tuple) of accepted page types
* Fix: Text areas in the non-default tab of the page editor now resize to the correct height
* Fix: Tabs in "insert link" modal in the rich text editor no longer disappear (Tim Heap)
* Fix: H2 elements in rich text fields were accidentally given a click() binding when put insite a collapsible multi field panel

View file

@ -143,7 +143,7 @@ FieldRowPanel
PageChooserPanel
----------------
.. class:: PageChooserPanel(field_name, model=None)
.. class:: PageChooserPanel(field_name, page_type=None)
You can explicitly link :class:`~wagtail.wagtailcore.models.Page`-derived models together using the :class:`~wagtail.wagtailcore.models.Page` model and ``PageChooserPanel``.
@ -166,7 +166,9 @@ PageChooserPanel
PageChooserPanel('related_page', 'demo.PublisherPage'),
]
``PageChooserPanel`` takes two arguments: a field name and an optional page type. Specifying a page type (in the form of an ``"appname.modelname"`` string) will filter the chooser to display only pages of that type.
``PageChooserPanel`` takes two arguments: a field name and an optional page type. Specifying a page type (in the form of an ``"appname.modelname"`` string) will filter the chooser to display only pages of that type. A list or tuple of page types can also be passed in, to allow choosing a page that matches any of those page types::
PageChooserPanel('related_page', ['demo.PublisherPage', 'demo.AuthorPage'])
ImageChooserPanel
-----------------

View file

@ -55,6 +55,7 @@ Minor features
* The ``update_index`` task now indexes objects in batches of 1000, to indicate progress and avoid excessive memory use
* Added database indexes on PageRevision and Image to improve performance on large sites
* Search in page chooser now uses Wagtail's search framework, to order results by relevance
* ``PageChooserPanel`` now supports passing a list (or tuple) of accepted page types
Bug fixes
~~~~~~~~~

View file

@ -555,19 +555,22 @@ class BasePageChooserPanel(BaseChooserPanel):
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))
target_models = []
cls._target_content_type = ContentType.objects.get_for_model(model)
for page_type in cls.page_type:
try:
target_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))
cls._target_content_type = list(ContentType.objects.get_for_models(*target_models).values())
else:
target_model = cls.model._meta.get_field(cls.field_name).rel.to
cls._target_content_type = ContentType.objects.get_for_model(target_model)
cls._target_content_type = [ContentType.objects.get_for_model(target_model)]
return cls._target_content_type
@ -575,6 +578,14 @@ class BasePageChooserPanel(BaseChooserPanel):
class PageChooserPanel(object):
def __init__(self, field_name, page_type=None):
self.field_name = field_name
if page_type:
# Convert single string/model into list
if not isinstance(page_type, (list, tuple)):
page_type = [page_type]
else:
page_type = []
self.page_type = page_type
def bind_to_model(self, model):

View file

@ -1,4 +1,4 @@
function createPageChooser(id, pageType, openAtParentId) {
function createPageChooser(id, pageTypes, openAtParentId) {
var chooserElement = $('#' + id + '-chooser');
var pageTitle = chooserElement.find('.title');
var input = $('#' + id);
@ -12,7 +12,7 @@ function createPageChooser(id, pageType, openAtParentId) {
ModalWorkflow({
url: initialUrl,
urlParams: { page_type: pageType },
urlParams: { page_type: pageTypes.join(',') },
responses: {
pageChosen: function(pageData) {
input.val(pageData.id);

View file

@ -1,20 +1,21 @@
{% load i18n %}
{% if page_types_restricted %}
{% trans "Choose" as choose_str %}
{% trans page_type_name as subtitle %}
{% else %}
{% trans "Choose a page" as choose_str %}
{% endif %}
{% include "wagtailadmin/shared/header.html" with title=choose_str subtitle=subtitle search_url="wagtailadmin_choose_page_search" query_parameters="page_type="|add:page_type_string icon="doc-empty-inverse" %}
{% include "wagtailadmin/shared/header.html" with title=choose_str subtitle=page_type_names|join:", " search_url="wagtailadmin_choose_page_search" query_parameters="page_type="|add:page_type_string icon="doc-empty-inverse" %}
<div class="nice-padding">
{% include 'wagtailadmin/chooser/_link_types.html' with current='internal' %}
{% if page_types_restricted %}
<p class="help-block help-warning">
{% blocktrans with type=page_type_name %}
{% blocktrans with type=page_type_names|join:", " count counter=page_type_names|length %}
Only pages of type "{{ type }}" may be chosen for this field. Search results will exclude pages of other types.
{% plural %}
Only the following page types may be chosen for this field: {{ type }}. Search results will exclude pages of other types.
{% endblocktrans %}
</p>
{% endif %}

View file

@ -367,7 +367,7 @@ class TestPageChooserPanel(TestCase):
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,7 +400,7 @@ class TestPageChooserPanel(TestCase):
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)
@ -414,7 +414,7 @@ class TestPageChooserPanel(TestCase):
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)
@ -423,7 +423,7 @@ class TestPageChooserPanel(TestCase):
result = PageChooserPanel(
'barbecue',
'wagtailcore.site'
).bind_to_model(PageChooserModel).target_content_type()
).bind_to_model(PageChooserModel).target_content_type()[0]
self.assertEqual(result.name, 'Site')
def test_target_content_type_malformed_type(self):

View file

@ -108,6 +108,33 @@ class TestChooserBrowseChild(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/chooser/browse.html')
def test_with_multiple_page_types(self):
# Add a page that is not a SimplePage
event_page = EventPage(
title="event",
slug="event",
)
self.root_page.add_child(instance=event_page)
# Send request
response = self.get({'page_type': 'tests.simplepage,tests.eventpage'})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/chooser/browse.html')
self.assertEqual(response.context['page_type_string'], 'tests.simplepage,tests.eventpage')
pages = {
page.id: page
for page in response.context['pages'].object_list
}
# Simple page in results, as before
self.assertIn(self.child_page.id, pages)
self.assertTrue(pages[self.child_page.id].can_choose)
# Event page should now also be choosable
self.assertIn(event_page.id, pages)
self.assertTrue(pages[self.child_page.id].can_choose)
def test_with_unknown_page_type(self):
response = self.get({'page_type': 'foo.bar'})
self.assertEqual(response.status_code, 404)
@ -215,6 +242,31 @@ class TestChooserSearch(TestCase, WagtailTestUtils):
self.assertContains(response, "There is one match")
self.assertContains(response, "foobarbaz")
def test_with_multiple_page_types(self):
# Add a page that is not a SimplePage
event_page = EventPage(
title="foo",
slug="foo",
)
self.root_page.add_child(instance=event_page)
# Send request
response = self.get({'q': "foo", 'page_type': 'tests.simplepage,tests.eventpage'})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/chooser/_search_results.html')
self.assertEqual(response.context['page_type_string'], 'tests.simplepage,tests.eventpage')
pages = {
page.id: page
for page in response.context['pages']
}
# Simple page in results, as before
self.assertIn(self.child_page.id, pages)
# Event page should now also be choosable
self.assertIn(event_page.id, pages)
def test_with_unknown_page_type(self):
response = self.get({'page_type': 'foo.bar'})
self.assertEqual(response.status_code, 404)

View file

@ -0,0 +1,64 @@
from django.test import TestCase
from django.contrib.contenttypes.models import ContentType
from wagtail.wagtailadmin import widgets
from wagtail.wagtailcore.models import Page
from wagtail.tests.testapp.models import SimplePage, EventPage
class TestAdminPageChooserWidget(TestCase):
def setUp(self):
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage(
title="foobarbaz",
slug="foobarbaz",
)
self.root_page.add_child(instance=self.child_page)
def test_render_html(self):
widget = widgets.AdminPageChooser()
html = widget.render_html('test', None, {})
self.assertIn("<input name=\"test\" type=\"hidden\" />", html)
def test_render_js_init(self):
widget = widgets.AdminPageChooser()
js_init = widget.render_js_init('test-id', 'test', None)
self.assertEqual(js_init, "createPageChooser(\"test-id\", [\"wagtailcore.page\"], null);")
def test_render_html_with_value(self):
widget = widgets.AdminPageChooser()
html = widget.render_html('test', self.child_page, {})
self.assertIn("<input name=\"test\" type=\"hidden\" value=\"%d\" />" % self.child_page.id, html)
def test_render_js_init_with_value(self):
widget = widgets.AdminPageChooser()
js_init = widget.render_js_init('test-id', 'test', self.child_page)
self.assertEqual(js_init, "createPageChooser(\"test-id\", [\"wagtailcore.page\"], %d);" % self.root_page.id)
# def test_render_html_init_with_content_type omitted as HTML does not
# change when selecting a content type
def test_render_js_init_with_content_type(self):
content_type = ContentType.objects.get_for_model(SimplePage)
widget = widgets.AdminPageChooser(content_type=content_type)
js_init = widget.render_js_init('test-id', 'test', None)
self.assertEqual(js_init, "createPageChooser(\"test-id\", [\"tests.simplepage\"], null);")
def test_render_js_init_with_multiple_content_types(self):
content_types = [
# Not using get_for_models as we need deterministic ordering
ContentType.objects.get_for_model(SimplePage),
ContentType.objects.get_for_model(EventPage),
]
widget = widgets.AdminPageChooser(content_type=content_types)
js_init = widget.render_js_init('test-id', 'test', None)
self.assertEqual(js_init, "createPageChooser(\"test-id\", [\"tests.simplepage\", \"tests.eventpage\"], null);")

View file

@ -29,13 +29,27 @@ def shared_context(request, extra_context={}):
return context
def page_model_from_string(string):
page_model = resolve_model_string(string)
def page_models_from_string(string):
page_models = []
if not issubclass(page_model, Page):
raise ValueError("Model is not a page")
for sub_string in string.split(','):
page_model = resolve_model_string(sub_string)
return page_model
if not issubclass(page_model, Page):
raise ValueError("Model is not a page")
page_models.append(page_model)
return tuple(page_models)
def filter_page_type(queryset, page_models):
qs = queryset.none()
for model in page_models:
qs |= queryset.type(model)
return qs
def browse(request, parent_page_id=None):
@ -53,21 +67,21 @@ def browse(request, parent_page_id=None):
page_type_string = request.GET.get('page_type') or 'wagtailcore.page'
if page_type_string != 'wagtailcore.page':
try:
desired_class = page_model_from_string(page_type_string)
desired_classes = page_models_from_string(page_type_string)
except (ValueError, LookupError):
raise Http404
# restrict the page listing to just those pages that:
# - are of the given content type (taking into account class inheritance)
# - or can be navigated into (i.e. have children)
choosable_pages = pages.type(desired_class)
choosable_pages = filter_page_type(pages, desired_classes)
descendable_pages = pages.filter(numchild__gt=0)
pages = choosable_pages | descendable_pages
else:
desired_class = Page
desired_classes = (Page, )
# Parent page can be chosen if it is a instance of desired_class
parent_page.can_choose = issubclass(parent_page.specific_class or Page, desired_class)
# Parent page can be chosen if it is a instance of desired_classes
parent_page.can_choose = issubclass(parent_page.specific_class or Page, desired_classes)
# Pagination
# We apply pagination first so we don't need to walk the entire list
@ -83,10 +97,10 @@ def browse(request, parent_page_id=None):
# Annotate each page with can_choose/can_decend flags
for page in pages:
if desired_class == Page:
if desired_classes == (Page, ):
page.can_choose = True
else:
page.can_choose = issubclass(page.specific_class or Page, desired_class)
page.can_choose = issubclass(page.specific_class or Page, desired_classes)
page.can_descend = page.get_children_count()
@ -99,7 +113,7 @@ def browse(request, parent_page_id=None):
'pages': pages,
'search_form': SearchForm(),
'page_type_string': page_type_string,
'page_type_name': desired_class.get_verbose_name(),
'page_type_names': [desired_class.get_verbose_name() for desired_class in desired_classes],
'page_types_restricted': (page_type_string != 'wagtailcore.page')
})
)
@ -110,17 +124,19 @@ def search(request, parent_page_id=None):
page_type_string = request.GET.get('page_type') or 'wagtailcore.page'
try:
desired_class = page_model_from_string(page_type_string)
desired_classes = page_models_from_string(page_type_string)
except (ValueError, LookupError):
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
).search(search_form.cleaned_data['q'], fields=['title'])[:10]
pages = filter_page_type(pages, desired_classes)
pages = pages[:10]
else:
pages = desired_class.objects.none()
pages = Page.objects.none()
shown_pages = []
for page in pages:

View file

@ -110,17 +110,24 @@ class AdminChooser(WidgetWithScript, widgets.Input):
class AdminPageChooser(AdminChooser):
target_content_type = 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):
super(AdminPageChooser, self).__init__(**kwargs)
self.target_content_type = content_type or ContentType.objects.get_for_model(Page)
self.target_content_types = content_type or ContentType.objects.get_for_model(Page)
# Make sure target_content_types is a list or tuple
if not isinstance(self.target_content_types, (list, tuple)):
self.target_content_types = [self.target_content_types]
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 +141,25 @@ class AdminPageChooser(AdminChooser):
})
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:
# Value is an ID look up object
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))