mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-04-21 07:10:59 +00:00
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:
commit
f65d91ad01
11 changed files with 209 additions and 46 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-----------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
64
wagtail/wagtailadmin/tests/test_widgets.py
Normal file
64
wagtail/wagtailadmin/tests/test_widgets.py
Normal 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);")
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue