kopia lustrzana https://github.com/wagtail/wagtail
allow panels & edit_handler to be defined on ModelAdmin (#4900)
rodzic
53cea8bc04
commit
4e8f197a9d
|
@ -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)
|
||||
|
|
|
@ -351,6 +351,7 @@ Contributors
|
|||
* Evan Winter
|
||||
* Neil Lyons
|
||||
* Gassan Gousseinov
|
||||
* Thomas Kremmel
|
||||
|
||||
Translators
|
||||
===========
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
)
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Ładowanie…
Reference in New Issue