allow panels & edit_handler to be defined on ModelAdmin (#4900)

pull/5123/head
Thomas Kremmel 2018-11-14 16:30:03 +01:00 zatwierdzone przez Matt Westcott
rodzic 53cea8bc04
commit 4e8f197a9d
12 zmienionych plików z 372 dodań i 11 usunięć

Wyświetl plik

@ -14,6 +14,7 @@ Changelog
* Added limit image upload size by number of pixels (Thomas Elliott)
* Added `manage.py wagtail_update_index` alias to avoid clashes with `update_index` commands from other packages (Matt Westcott)
* Renamed `target_model` argument on `PageChooserBlock` to `page_type` (Loic Teixeira)
* `edit_handler` and `panels` can now be defined on a `ModelAdmin` definition (Thomas Kremmel)
* Fix: Set `SERVER_PORT` to 443 in `Page.dummy_request()` for HTTPS sites (Sergey Fedoseev)
* Fix: Include port number in `Host` header of `Page.dummy_request()` (Sergey Fedoseev)
* Fix: Validation error messages in `InlinePanel` no longer count towards `max_num` when disabling the 'add' button (Todd Dembrey, Thibaud Colas)

Wyświetl plik

@ -351,6 +351,7 @@ Contributors
* Evan Winter
* Neil Lyons
* Gassan Gousseinov
* Thomas Kremmel
Translators
===========

Wyświetl plik

@ -58,6 +58,38 @@ Or alternatively:
# or
edit_handler = TabbedInterface([ObjectList(custom_panels), ObjectList(...)])
.. versionadded:: 2.5
``edit_handler`` and ``panels`` can alternatively be
defined on a ``ModelAdmin`` definition. This feature is especially useful
for use cases where you have to work with models that are
'out of reach' (due to being part of a third-party package, for example).
.. code-block:: python
class BookAdmin(ModelAdmin):
model = Book
panels = [
FieldPanel('title'),
FieldPanel('author'),
]
Or alternatively:
.. code-block:: python
class BookAdmin(ModelAdmin):
model = Book
custom_panels = [
FieldPanel('title'),
FieldPanel('author'),
]
edit_handler = ObjectList(custom_panels)
.. _modeladmin_form_view_extra_css:
-----------------------------------
@ -164,3 +196,17 @@ value will be passed to the edit form, so that any named fields will be
excluded from the form. This is particularly useful when registering ModelAdmin
classes for models from third-party apps, where defining panel configurations
on the Model itself is more complicated.
-----------------------------------
``ModelAdmin.get_edit_handler()``
-----------------------------------
.. versionadded:: 2.5
**Must return**: An instance of ``wagtail.admin.edit_handlers.ObjectList``
Returns the appropriate ``edit_handler`` for the modeladmin class.
``edit_handlers`` can be defined either on the model itself or on the
modeladmin (as property ``edit_handler`` or ``panels``). Falls back to
extracting panel / edit handler definitions from the model class.

Wyświetl plik

@ -24,6 +24,7 @@ Other features
* Added limit image upload size by number of pixels (Thomas Elliott)
* Added ``manage.py wagtail_update_index`` alias to avoid clashes with ``update_index`` commands from other packages (Matt Westcott)
* Renamed ``target_model`` argument on ``PageChooserBlock`` to ``page_type`` (Loic Teixeira)
* ``edit_handler`` and ``panels`` can now be defined on a ``ModelAdmin`` definition (Thomas Kremmel)
Bug fixes

Wyświetl plik

@ -6,6 +6,7 @@ from django.db.models import Model
from django.utils.safestring import mark_safe
from wagtail.admin.checks import check_panels_in_model
from wagtail.admin.edit_handlers import ObjectList, extract_panel_definitions_from_model_class
from wagtail.core import hooks
from wagtail.core.models import Page
@ -372,6 +373,29 @@ class ModelAdmin(WagtailRegisterable):
view_class = self.delete_view_class
return view_class.as_view(**kwargs)(request)
def get_edit_handler(self, instance, request):
"""
Returns the appropriate edit_handler for this modeladmin class.
edit_handlers can be defined either on the model itself or on the
modeladmin (as property edit_handler or panels). Falls back to
extracting panel / edit handler definitions from the model class.
"""
if hasattr(self, 'edit_handler'):
edit_handler = self.edit_handler
elif hasattr(self, 'panels'):
panels = self.panels
edit_handler = ObjectList(panels)
elif hasattr(self.model, 'edit_handler'):
edit_handler = self.model.edit_handler
elif hasattr(self.model, 'panels'):
panels = self.model.panels
edit_handler = ObjectList(panels)
else:
fields_to_exclude = self.get_form_fields_exclude(request=request)
panels = extract_panel_definitions_from_model_class(self.model, exclude=fields_to_exclude)
edit_handler = ObjectList(panels)
return edit_handler
def get_templates(self, action='index'):
"""
Utility funtion that provides a list of templates to try for a given

Wyświetl plik

@ -0,0 +1,131 @@
from unittest import mock
from django.test import RequestFactory, TestCase
from wagtail.admin.edit_handlers import FieldPanel, ObjectList, TabbedInterface
from wagtail.contrib.modeladmin.views import CreateView
from wagtail.tests.modeladmintest.models import Person
from wagtail.tests.modeladmintest.wagtail_hooks import PersonAdmin
from wagtail.tests.utils import WagtailTestUtils
class TestExtractPanelDefinitionsFromModelAdmin(TestCase, WagtailTestUtils):
"""tests that edit_handler and panels can be defined on modeladmin"""
def setUp(self):
self.factory = RequestFactory()
self.user = self.create_test_user()
self.login(user=self.user)
def test_model_edit_handler(self):
"""loads the 'create' view and verifies that form fields are returned
which have been defined via model Person.edit_handler"""
response = self.client.get('/admin/modeladmintest/person/create/')
self.assertEqual(
[field_name for field_name in response.context['form'].fields],
['first_name', 'last_name', 'phone_number']
)
@mock.patch('wagtail.contrib.modeladmin.views.ModelFormView.get_edit_handler')
def test_model_form_view_edit_handler_called(self, mock_modelformview_get_edit_handler):
"""loads the ``create`` view and verifies that modelformview edit_handler is called"""
self.client.get('/admin/modeladmintest/person/create/')
self.assertGreater(len(mock_modelformview_get_edit_handler.call_args_list), 0)
@mock.patch('wagtail.contrib.modeladmin.options.ModelAdmin.get_edit_handler')
def test_model_admin_edit_handler_called(self, mock_modeladmin_get_edit_handler):
"""loads the ``create`` view and verifies that modeladmin edit_handler is called"""
# constructing the request in order to be able to assert it
request = self.factory.get('/admin/modeladmintest/person/create/')
request.user = self.user
view = CreateView.as_view(model_admin=PersonAdmin())
view(request)
edit_handler_call = mock_modeladmin_get_edit_handler.call_args_list[0]
call_args, call_kwargs = edit_handler_call
# not using CreateView.get_instance since
# CreateView.get_instance always returns a new instance
self.assertEqual(type(call_kwargs['instance']), Person)
self.assertEqual(call_kwargs['request'], request)
def test_model_panels(self):
"""loads the 'create' view and verifies that form fields are returned
which have been defined via model Friend.panels"""
response = self.client.get('/admin/modeladmintest/friend/create/')
self.assertEqual(
[field_name for field_name in response.context['form'].fields],
['first_name', 'phone_number']
)
def test_model_admin_edit_handler(self):
"""loads the 'create' view and verifies that form fields are returned
which have been defined via model VisitorAdmin.edit_handler"""
response = self.client.get('/admin/modeladmintest/visitor/create/')
self.assertEqual(
[field_name for field_name in response.context['form'].fields],
['last_name', 'phone_number', 'address']
)
def test_model_admin_panels(self):
"""loads the 'create' view and verifies that form fields are returned
which have been defined via model ContributorAdmin.panels"""
response = self.client.get('/admin/modeladmintest/contributor/create/')
self.assertEqual(
[field_name for field_name in response.context['form'].fields],
['last_name', 'phone_number', 'address']
)
def test_model_admin_panel_edit_handler_priority(self):
"""verifies that model admin panels are preferred over model panels"""
# check if Person panel or edit_handler definition is used for
# form creation, since PersonAdmin has neither panels nor an
# edit_handler defined
model_admin = PersonAdmin()
edit_handler = model_admin.get_edit_handler(None, None)
edit_handler = edit_handler.bind_to(model=model_admin.model)
form_class = edit_handler.get_form_class()
form = form_class()
self.assertEqual(
[field_name for field_name in form.fields],
['first_name', 'last_name', 'phone_number']
)
# now add a panel definition to the PersonAdmin and verify that
# panel definition from PersonAdmin is used to construct the form
# and NOT the panel or edit_handler definition from the Person model
model_admin = PersonAdmin()
model_admin.panels = [
FieldPanel('last_name'),
FieldPanel('phone_number'),
FieldPanel('address'),
]
edit_handler = model_admin.get_edit_handler(None, None)
edit_handler = edit_handler.bind_to(model=model_admin.model)
form_class = edit_handler.get_form_class()
form = form_class()
self.assertEqual(
[field_name for field_name in form.fields],
['last_name', 'phone_number', 'address']
)
# now add a edit_handler definition to the PersonAdmin and verify that
# edit_handler definition from PersonAdmin is used to construct the
# form and NOT the panel or edit_handler definition from the
# Person model
model_admin = PersonAdmin()
model_admin.edit_handler = TabbedInterface([
ObjectList(
[
FieldPanel('phone_number'),
FieldPanel('address'),
]
),
])
edit_handler = model_admin.get_edit_handler(None, None)
edit_handler = edit_handler.bind_to(model=model_admin.model)
form_class = edit_handler.get_form_class()
form = form_class()
self.assertEqual(
[field_name for field_name in form.fields],
['phone_number', 'address']
)

Wyświetl plik

@ -187,7 +187,7 @@ class TestCreateView(TestCase, WagtailTestUtils):
def test_exclude_passed_to_extract_panel_definitions(self):
path_to_form_fields_exclude_property = 'wagtail.contrib.modeladmin.options.ModelAdmin.form_fields_exclude'
with mock.patch('wagtail.contrib.modeladmin.views.extract_panel_definitions_from_model_class') as m:
with mock.patch('wagtail.contrib.modeladmin.options.extract_panel_definitions_from_model_class') as m:
with mock.patch(path_to_form_fields_exclude_property, new_callable=mock.PropertyMock) as mock_form_fields_exclude:
mock_form_fields_exclude.return_value = ['123']
@ -315,7 +315,7 @@ class TestEditView(TestCase, WagtailTestUtils):
def test_exclude_passed_to_extract_panel_definitions(self):
path_to_form_fields_exclude_property = 'wagtail.contrib.modeladmin.options.ModelAdmin.form_fields_exclude'
with mock.patch('wagtail.contrib.modeladmin.views.extract_panel_definitions_from_model_class') as m:
with mock.patch('wagtail.contrib.modeladmin.options.extract_panel_definitions_from_model_class') as m:
with mock.patch(path_to_form_fields_exclude_property, new_callable=mock.PropertyMock) as mock_form_fields_exclude:
mock_form_fields_exclude.return_value = ['123']

Wyświetl plik

@ -28,7 +28,6 @@ from django.views.generic import TemplateView
from django.views.generic.edit import FormView
from wagtail.admin import messages
from wagtail.admin.edit_handlers import ObjectList, extract_panel_definitions_from_model_class
from .forms import ParentChooserForm
@ -115,13 +114,10 @@ class WMABaseView(TemplateView):
class ModelFormView(WMABaseView, FormView):
def get_edit_handler(self):
if hasattr(self.model, 'edit_handler'):
edit_handler = self.model.edit_handler
else:
fields_to_exclude = self.model_admin.get_form_fields_exclude(request=self.request)
panels = extract_panel_definitions_from_model_class(self.model, exclude=fields_to_exclude)
edit_handler = ObjectList(panels)
return edit_handler.bind_to(model=self.model)
edit_handler = self.model_admin.get_edit_handler(
instance=self.get_instance(), request=self.request
)
return edit_handler.bind_to(model=self.model_admin.model)
def get_form_class(self):
return self.get_edit_handler().get_form_class()

Wyświetl plik

@ -0,0 +1,43 @@
# Generated by Django 2.1.3 on 2018-11-14 15:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('modeladmintest', '0005_book_cover_image'),
]
operations = [
migrations.CreateModel(
name='Contributor',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
('phone_number', models.CharField(max_length=255)),
('address', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='Person',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
('phone_number', models.CharField(max_length=255)),
('address', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='Visitor',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
('phone_number', models.CharField(max_length=255)),
('address', models.CharField(max_length=255)),
],
),
]

Wyświetl plik

@ -0,0 +1,23 @@
# Generated by Django 2.1.3 on 2018-12-18 12:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('modeladmintest', '0006_contributor_person_visitor'),
]
operations = [
migrations.CreateModel(
name='Friend',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
('phone_number', models.CharField(max_length=255)),
('address', models.CharField(max_length=255)),
],
),
]

Wyświetl plik

@ -1,5 +1,6 @@
from django.db import models
from wagtail.admin.edit_handlers import FieldPanel, ObjectList, TabbedInterface
from wagtail.core.models import Page
from wagtail.search import index
@ -46,3 +47,61 @@ class Publisher(models.Model):
class VenuePage(Page):
address = models.CharField(max_length=300)
capacity = models.IntegerField()
class Visitor(models.Model):
"""model used to test modeladmin.edit_handler usage in get_edit_handler"""
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
phone_number = models.CharField(max_length=255)
address = models.CharField(max_length=255)
def __str__(self):
return self.first_name
class Contributor(models.Model):
"""model used to test modeladmin.panels usage in get_edit_handler"""
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
phone_number = models.CharField(max_length=255)
address = models.CharField(max_length=255)
def __str__(self):
return self.first_name
class Person(models.Model):
"""model used to test model.edit_handlers usage in get_edit_handler"""
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
phone_number = models.CharField(max_length=255)
address = models.CharField(max_length=255)
panels = [
FieldPanel('first_name'),
FieldPanel('last_name'),
FieldPanel('phone_number'),
]
edit_handler = TabbedInterface([
ObjectList(panels),
])
def __str__(self):
return self.first_name
class Friend(models.Model):
"""model used to test model.panels usage in get_edit_handler"""
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
phone_number = models.CharField(max_length=255)
address = models.CharField(max_length=255)
panels = [
FieldPanel('first_name'),
FieldPanel('phone_number'),
]
def __str__(self):
return self.first_name

Wyświetl plik

@ -1,10 +1,11 @@
from wagtail.admin.edit_handlers import FieldPanel, ObjectList, TabbedInterface
from wagtail.contrib.modeladmin.options import (
ModelAdmin, ModelAdminGroup, ThumbnailMixin, modeladmin_register)
from wagtail.contrib.modeladmin.views import CreateView
from wagtail.tests.testapp.models import BusinessChild, EventPage, SingleEventPage
from .forms import PublisherModelAdminForm
from .models import Author, Book, Publisher, Token, VenuePage
from .models import Author, Book, Contributor, Friend, Person, Publisher, Token, VenuePage, Visitor
class AuthorModelAdmin(ModelAdmin):
@ -91,6 +92,37 @@ class VenuePageAdmin(ModelAdmin):
exclude_from_explorer = True
class PersonAdmin(ModelAdmin):
model = Person
class FriendAdmin(ModelAdmin):
model = Friend
class VisitorAdmin(ModelAdmin):
model = Visitor
panels = [
FieldPanel('last_name'),
FieldPanel('phone_number'),
FieldPanel('address'),
]
edit_handler = TabbedInterface([
ObjectList(panels),
])
class ContributorAdmin(ModelAdmin):
model = Contributor
panels = [
FieldPanel('last_name'),
FieldPanel('phone_number'),
FieldPanel('address'),
]
class EventsAdminGroup(ModelAdminGroup):
menu_label = "Events"
items = (EventPageAdmin, SingleEventPageAdmin, VenuePageAdmin)
@ -109,3 +141,7 @@ modeladmin_register(TokenModelAdmin)
modeladmin_register(PublisherModelAdmin)
modeladmin_register(EventsAdminGroup)
modeladmin_register(BusinessChildAdmin)
modeladmin_register(PersonAdmin)
modeladmin_register(FriendAdmin)
modeladmin_register(VisitorAdmin)
modeladmin_register(ContributorAdmin)