Added wagtailmodeladmin to contrib with docs

pull/2165/merge
Andy Babic 2016-04-13 07:01:04 +01:00 zatwierdzone przez Matt Westcott
rodzic a31d4d32ce
commit dcb67dcfdf
47 zmienionych plików z 3732 dodań i 8 usunięć

Wyświetl plik

@ -4,11 +4,12 @@ Changelog
1.5 (xx.xx.xxxx)
~~~~~~~~~~~~~~~~
* Added wagtail.contrib.modeladmin, an app for configuring arbitrary Django models to be edited through the Wagtail admin (Andy Babic)
* The "dynamic serve view" for images has been greatly improved. See release notes for details
* Moved lesser-user actions in the page explorer into a 'More' dropdown
* Added a hook `register_page_listing_buttons` for adding action buttons to the page explorer
* Added 'revisions' action to pages list (Roel Bruggink)
* Added jinja2 support for the ``settings`` template tag (Tim Heap)
* The "dynamic serve view" for images has been greatly improved. See release notes for details
* Added a hook `insert_global_admin_js` for inserting custom JavaScript throughout the admin backend (Tom Dyson)
* Recognise instagram embed URLs with `www` prefix (Matt Westcott)
* The type of the ``search_fields`` attribute on ``Page`` models (and other searchable models) has changed from a tuple to a list (Tim Heap)

Wyświetl plik

@ -122,6 +122,7 @@ Contributors
* Janneke Janssen
* Roel Bruggink
* Yannick Chabbert
* Andy Babic
Translators
===========

Wyświetl plik

@ -14,6 +14,7 @@ Wagtail ships with a variety of extra optional modules.
frontendcache
routablepage
api/index
modeladmin
searchpromotions
@ -59,7 +60,13 @@ Provides a way of embedding Django URLconfs into pages.
A module for adding a read only, JSON based web API to your Wagtail site
:doc:`modeladmin`
-----------------
A module allowing for more customisable representation and management of custom models in Wagtail's admin area.
:doc:`searchpromotions`
-----------------------
A module for managing "Promoted Search Results"
A module for managing "Promoted Search Results".

Wyświetl plik

@ -0,0 +1,220 @@
.. _modeladmin_intro:
=====================
``ModelAdmin``
=====================
The ``modeladmin`` module allows you to create customisable listing
pages for any model in your Wagtail project, and add navigation elements to the
Wagtail admin area so that you can reach them. Simply extend the ``ModelAdmin``
class, override a few attributes to suit your needs, register it with Wagtail
using an easy one-line method (you can copy and paste from the examples below),
and you're good to go.
You can use it with any Django model (it doesnt need to extend ``Page`` or
be registered as a ``Snippet``), and it wont interfere with any of the
existing admin functionality that Wagtail provides.
.. _modeladmin_features:
A full list of features
-----------------------
- A customisable list view, allowing you to control what values are displayed
for each row, available options for result filtering, default ordering, and
more.
- Access your list views from the Wagtail admin menu easily with automatically
generated menu items, with automatic 'active item' highlighting. Control the
label text and icons used with easy-to-change attributes on your class.
- An additional ``ModelAdminGroup`` class, that allows you to group your
related models, and list them together in their own submenu, for a more
logical user experience.
- Simple, robust **add** and **edit** views for your non-Page models that use
the panel configurations defined on your model using Wagtail's edit panels.
- For Page models, the system directs to Wagtail's existing add and
edit views, and returns you back to the correct list page, for a seamless
experience.
- Full respect for permissions assigned to your Wagtail users and groups. Users
will only be able to do what you want them to!
- All you need to easily hook your ``ModelAdmin`` classes into Wagtail, taking
care of URL registration, menu changes, and registering any missing model
permissions, so that you can assign them to Groups.
- **Built to be customisable** - While ``modeladmin`` provides a solid
experience out of the box, you can easily use your own templates, and the
``ModelAdmin`` class has a large number of methods that you can override or
extend, allowing you to customise the behaviour to a greater degree.
.. _modeladmin_usage:
Installation
------------
Add ``wagtail.contrib.modeladmin`` to your ``INSTALLED_APPS``:
.. code-block:: python
INSTALLED_APPS = [
...
'wagtail.contrib.modeladmin',
]
How to use
----------
.. _modeladmin_example_simple:
A simple example
^^^^^^^^^^^^^^^^
You have a model in your app, and you want a listing page specifically for that
model, with a menu item added to the menu in the Wagtail admin area so that you
can get to it.
``wagtail_hooks.py`` in your app directory would look something like this:
.. code-block:: python
from wagtail.contrib.modeladmin.options import (
ModelAdmin, modeladmin_register)
from .models import MyPageModel
class MyPageModelAdmin(ModelAdmin):
model = MyPageModel
menu_label = 'Page Model' # ditch this to use verbose_name_plural from model
menu_icon = 'date' # change as required
menu_order = 200 # will put in 3rd place (000 being 1st, 100 2nd)
add_to_settings_menu = False # or True to add your model to the Settings sub-menu
list_display = ('title', 'example_field2', 'example_field3', 'live')
list_filter = ('live', 'example_field2', 'example_field3')
search_fields = ('title',)
# Now you just need to register your customised ModelAdmin class with Wagtail
modeladmin_register(MyPageModelAdmin)
.. _modeladmin_example_complex:
A more complicated example
^^^^^^^^^^^^^^^^^^^^^^^^^^
You have an app with several models that you want to show grouped together in
Wagtail's admin menu. Some of the models might extend Page, and others might
be simpler models, perhaps registered as Snippets, perhaps not. No problem!
ModelAdminGroup allows you to group them all together nicely.
``wagtail_hooks.py`` in your app directory would look something like this:
.. code-block:: python
from wagtail.contrib.modeladmin.options import (
ModelAdmin, ModelAdminGroup, modeladmin_register)
from .models import (
MyPageModel, MyOtherPageModel, MySnippetModel, SomeOtherModel)
class MyPageModelAdmin(ModelAdmin):
model = MyPageModel
menu_label = 'Page Model' # ditch this to use verbose_name_plural from model
menu_icon = 'doc-full-inverse' # change as required
list_display = ('title', 'example_field2', 'example_field3', 'live')
list_filter = ('live', 'example_field2', 'example_field3')
search_fields = ('title',)
class MyOtherPageModelAdmin(ModelAdmin):
model = MyOtherPageModel
menu_label = 'Other Page Model' # ditch this to use verbose_name_plural from model
menu_icon = 'doc-full-inverse' # change as required
list_display = ('title', 'example_field2', 'example_field3', 'live')
list_filter = ('live', 'example_field2', 'example_field3')
search_fields = ('title',)
class MySnippetModelAdmin(ModelAdmin):
model = MySnippetModel
menu_label = 'Snippet Model' # ditch this to use verbose_name_plural from model
menu_icon = 'snippet' # change as required
list_display = ('title', 'example_field2', 'example_field3')
list_filter = (example_field2', 'example_field3')
search_fields = ('title',)
class SomeOtherModelAdmin(ModelAdmin):
model = SomeOtherModel
menu_label = 'Some other model' # ditch this to use verbose_name_plural from model
menu_icon = 'snippet' # change as required
list_display = ('title', 'example_field2', 'example_field3')
list_filter = (example_field2', 'example_field3')
search_fields = ('title',)
class MyModelAdminGroup(ModelAdminGroup):
menu_label = 'My App'
menu_icon = 'folder-open-inverse' # change as required
menu_order = 200 # will put in 3rd place (000 being 1st, 100 2nd)
items = (MyPageModelAdmin, MyOtherPageModelAdmin, MySnippetModelAdmin, SomeOtherModelAdmin)
# When using a ModelAdminGroup class to group several ModelAdmin classes together,
# you only need to register the ModelAdminGroup class with Wagtail:
modeladmin_register(MyModelAdminGroup)
.. _modeladmin_multi_registeration:
Registering multiple classes in one ``wagtail_hooks.py`` file
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you have an app with more than one model that you wish to manage, or even
multiple models you wish to group together with ``ModelAdminGroup`` classes,
that's possible. Just register each of your ModelAdmin classes using
``modeladmin_register``, and they'll work as expected.
.. code-block:: python
class MyPageModelAdmin(ModelAdmin):
model = MyPageModel
...
class MyOtherPageModelAdmin(ModelAdmin):
model = MyOtherPageModel
...
class MyModelAdminGroup(ModelAdminGroup):
label = _("Group 1")
items = (ModelAdmin1, ModelAdmin2)
...
class MyOtherModelAdminGroup(ModelAdminGroup):
label = _("Group 2")
items = (ModelAdmin3, ModelAdmin4)
...
modeladmin_register(MyPageModelAdmin)
modeladmin_register(MyOtherPageModelAdmin)
modeladmin_register(MyModelAdminGroup)
modeladmin_register(MyOtherModelAdminGroup)
Supported list options
-----------------------
With the exception of bulk actions and date hierarchy, the ``ModelAdmin`` class
offers similar list functionality to Django's ``ModelAdmin`` class, providing:
- control over what values are displayed (via the ``list_display`` attribute)
- control over default ordering (via the ``ordering`` attribute)
- customisable model-specific text search (via the ``search_fields`` attribute)
- customisable filters (via the ``list_filter`` attribue)
``list_display`` supports the same fields and methods as Django's ModelAdmin
class (including ``short_description`` and ``admin_order_field`` on custom
methods), giving you lots of flexibility when it comes to output.
`Read more about list_display in the Django docs <https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display>`_.
``list_filter`` supports the same field types as Django's ModelAdmin class,
giving your users an easy way to find what they're looking for.
`Read more about list_filter in the Django docs <https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter>`_.

Wyświetl plik

@ -10,6 +10,12 @@ Wagtail 1.5 release notes - IN DEVELOPMENT
What's new
==========
ModelAdmin
~~~~~~~~~~
Wagtail now includes an app ``wagtail.contrib.modeladmin`` (previously available separately as the `wagtailmodeladmin <https://github.com/rkhleics/wagtailmodeladmin>`_ package) which allows you to configure arbitrary Django models to be listed, added and edited through the Wagtail admin. See :doc:`/reference/contrib/modeladmin` for full documentation. This feature was developed by Andy Babic.
Improvements to the "Image serve view"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Wyświetl plik

@ -33,6 +33,13 @@ var apps = [
new App('wagtail/contrib/settings', {
'appName': 'wagtailsettings',
}),
new App('wagtail/contrib/modeladmin', {
'scss': [
'modeladmin/scss/index.scss',
'modeladmin/scss/choose_parent_page.scss',
],
}),
];
module.exports = {

Wyświetl plik

@ -0,0 +1 @@
static/

Wyświetl plik

@ -0,0 +1,30 @@
from __future__ import absolute_import, unicode_literals
from django import forms
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from wagtail.wagtailcore.models import Page
class PageChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
bits = []
for ancestor in obj.get_ancestors(inclusive=True).exclude(depth=1):
bits.append(ancestor.title)
return mark_safe('<span class="icon icon-arrow-right"></span>'.join(bits))
class ParentChooserForm(forms.Form):
parent_page = PageChoiceField(
label=_('Parent page'),
required=True,
empty_label=None,
queryset=Page.objects.none(),
widget=forms.RadioSelect(),
)
def __init__(self, valid_parents_qs, *args, **kwargs):
self.valid_parents_qs = valid_parents_qs
super(ParentChooserForm, self).__init__(*args, **kwargs)
self.fields['parent_page'].queryset = self.valid_parents_qs

Wyświetl plik

@ -0,0 +1,362 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.admin.utils import quote
from django.contrib.auth import get_permission_codename
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.utils.encoding import force_text
from django.utils.functional import cached_property
from django.utils.http import urlquote
from django.utils.translation import ugettext as _
from wagtail.wagtailcore.models import Page
class AdminURLHelper(object):
def __init__(self, model):
self.model = model
self.opts = model._meta
def _get_action_url_pattern(self, action):
if action == 'index':
return r'^%s/%s/$' % (self.opts.app_label, self.opts.model_name)
return r'^%s/%s/%s/$' % (self.opts.app_label, self.opts.model_name,
action)
def _get_object_specific_action_url_pattern(self, action):
return r'^%s/%s/%s/(?P<instance_pk>[-\w]+)/$' % (
self.opts.app_label, self.opts.model_name, action)
def get_action_url_pattern(self, action):
if action in ('create', 'choose_parent', 'index'):
return self._get_action_url_pattern(action)
return self._get_object_specific_action_url_pattern(action)
def get_action_url_name(self, action):
return '%s_%s_modeladmin_%s' % (
self.opts.app_label, self.opts.model_name, action)
def get_action_url(self, action, *args, **kwargs):
if action in ('create', 'choose_parent', 'index'):
return reverse(self.get_action_url_name(action))
url_name = self.get_action_url_name(action)
return reverse(url_name, args=args, kwargs=kwargs)
@cached_property
def index_url(self):
return self.get_action_url('index')
@cached_property
def create_url(self):
return self.get_action_url('create')
class PageAdminURLHelper(AdminURLHelper):
def get_action_url(self, action, *args, **kwargs):
if action in ('add', 'edit', 'delete', 'unpublish', 'copy'):
url_name = 'wagtailadmin_pages:%s' % action
target_url = reverse(url_name, args=args, kwargs=kwargs)
return '%s?next=%s' % (target_url, urlquote(self.index_url))
return super(PageAdminURLHelper, self).get_action_url(action, *args,
**kwargs)
class PermissionHelper(object):
"""
Provides permission-related helper functions to help determine what a
user can do with a 'typical' model (where permissions are granted
model-wide), and to a specific instance of that model.
"""
def __init__(self, model, inspect_view_enabled=False):
self.model = model
self.opts = model._meta
self.inspect_view_enabled = inspect_view_enabled
def get_all_model_permissions(self):
"""
Return a queryset of all Permission objects pertaining to the `model`
specified at initialisation.
"""
return Permission.objects.filter(
content_type__app_label=self.opts.app_label,
content_type__model=self.opts.model_name,
)
def get_perm_codename(self, action):
return get_permission_codename(action, self.opts)
def user_has_specific_permission(self, user, perm_codename):
"""
Combine `perm_codename` with `self.opts.app_label` to call the provided
Django user's built-in `has_perm` method.
"""
return user.has_perm("%s.%s" % (self.opts.app_label, perm_codename))
def user_has_any_permissions(self, user):
"""
Return a boolean to indicate whether `user` has any model-wide
permissions
"""
for perm in self.get_all_model_permissions().values('codename'):
if self.user_has_specific_permission(user, perm['codename']):
return True
return False
def user_can_list(self, user):
"""
Return a boolean to indicate whether `user` is permitted to access the
list view for self.model
"""
return self.user_has_any_permissions(user)
def user_can_create(self, user):
"""
Return a boolean to indicate whether `user` is permitted to create new
instances of `self.model`
"""
perm_codename = self.get_perm_codename('add')
return self.user_has_specific_permission(user, perm_codename)
def user_can_inspect_obj(self, user, obj):
"""
Return a boolean to indicate whether `user` is permitted to 'inspect'
a specific `self.model` instance.
"""
return self.inspect_view_enabled and self.user_has_any_permissions(
user)
def user_can_edit_obj(self, user, obj):
"""
Return a boolean to indicate whether `user` is permitted to 'change'
a specific `self.model` instance.
"""
perm_codename = self.get_perm_codename('change')
return self.user_has_specific_permission(user, perm_codename)
def user_can_delete_obj(self, user, obj):
"""
Return a boolean to indicate whether `user` is permitted to 'delete'
a specific `self.model` instance.
"""
perm_codename = self.get_perm_codename('delete')
return self.user_has_specific_permission(user, perm_codename)
def user_can_unpublish_obj(self, user, obj):
return False
def user_can_copy_obj(self, user, obj):
return False
class PagePermissionHelper(PermissionHelper):
"""
Provides permission-related helper functions to help determine what
a user can do with a model extending Wagtail's Page model. It differs
from `PermissionHelper`, because model-wide permissions aren't really
relevant. We generally need to determine permissions on an
object-specific basis.
"""
def get_valid_parent_pages(self, user):
"""
Identifies possible parent pages for the current user by first looking
at allowed_parent_page_models() on self.model to limit options to the
correct type of page, then checking permissions on those individual
pages to make sure we have permission to add a subpage to it.
"""
# Get queryset of pages where this page type can be added
allowed_parent_page_content_types = list(ContentType.objects.get_for_models(*self.model.allowed_parent_page_models()).values())
allowed_parent_pages = Page.objects.filter(content_type__in=allowed_parent_page_content_types)
# Get queryset of pages where the user has permission to add subpages
if user.is_superuser:
pages_where_user_can_add = Page.objects.all()
else:
pages_where_user_can_add = Page.objects.none()
user_perms = UserPagePermissionsProxy(user)
for perm in user_perms.permissions.filter(permission_type='add'):
# user has add permission on any subpage of perm.page
# (including perm.page itself)
pages_where_user_can_add |= Page.objects.descendant_of(perm.page, inclusive=True)
# Combine them
return allowed_parent_pages & pages_where_user_can_add
def user_can_list(self, user):
"""
For models extending Page, permitted actions are determined by
permissions on individual objects. Rather than check for change
permissions on every object individually (which would be quite
resource intensive), we simply always allow the list view to be
viewed, and limit further functionality when relevant.
"""
return True
def user_can_create(self, user):
"""
For models extending Page, whether or not a page of this type can be
added somewhere in the tree essentially determines the add permission,
rather than actual model-wide permissions
"""
return self.get_valid_parent_pages(user).exists()
def user_can_edit_obj(self, user, obj):
perms = obj.permissions_for_user(user)
return perms.can_edit()
def user_can_delete_obj(self, user, obj):
perms = obj.permissions_for_user(user)
return perms.can_delete()
def user_can_publish_obj(self, user, obj):
perms = obj.permissions_for_user(user)
return obj.live and perms.can_unpublish()
def user_can_copy_obj(self, user, obj):
parent_page = obj.get_parent()
return parent_page.permissions_for_user(user).can_publish_subpage()
class ButtonHelper(object):
default_button_classnames = ['button']
add_button_classnames = ['bicolor', 'icon', 'icon-plus']
inspect_button_classnames = []
edit_button_classnames = []
delete_button_classnames = ['no']
def __init__(self, view, request):
self.view = view
self.request = request
self.model = view.model
self.opts = view.model._meta
self.verbose_name = force_text(self.opts.verbose_name)
self.verbose_name_plural = force_text(self.opts.verbose_name_plural)
self.permission_helper = view.permission_helper
self.url_helper = view.url_helper
def finalise_classname(self, classnames_add=[], classnames_exclude=[]):
combined = self.default_button_classnames + classnames_add
finalised = [cn for cn in combined if cn not in classnames_exclude]
return ' '.join(finalised)
def add_button(self, classnames_add=[], classnames_exclude=[]):
classnames = self.add_button_classnames + classnames_add
cn = self.finalise_classname(classnames, classnames_exclude)
return {
'url': self.url_helper.create_url,
'label': _('Add %s') % self.verbose_name,
'classname': cn,
'title': _('Add a new %s') % self.verbose_name,
}
def inspect_button(self, pk, classnames_add=[], classnames_exclude=[]):
classnames = self.inspect_button_classnames + classnames_add
cn = self.finalise_classname(classnames, classnames_exclude)
return {
'url': self.url_helper.get_action_url('inspect', quote(pk)),
'label': _('Inspect'),
'classname': cn,
'title': _('Inspect this %s') % self.verbose_name,
}
def edit_button(self, pk, classnames_add=[], classnames_exclude=[]):
classnames = self.edit_button_classnames + classnames_add
cn = self.finalise_classname(classnames, classnames_exclude)
return {
'url': self.url_helper.get_action_url('edit', quote(pk)),
'label': _('Edit'),
'classname': cn,
'title': _('Edit this %s') % self.verbose_name,
}
def delete_button(self, pk, classnames_add=[], classnames_exclude=[]):
classnames = self.delete_button_classnames + classnames_add
cn = self.finalise_classname(classnames, classnames_exclude)
return {
'url': self.url_helper.get_action_url('delete', quote(pk)),
'label': _('Delete'),
'classname': cn,
'title': _('Delete this %s') % self.verbose_name,
}
def get_buttons_for_obj(self, obj, exclude=[], classnames_add=[],
classnames_exclude=[]):
ph = self.permission_helper
usr = self.request.user
pk = quote(getattr(obj, self.opts.pk.attname))
btns = []
if('inspect' not in exclude and ph.user_can_inspect_obj(usr, obj)):
btns.append(
self.inspect_button(pk, classnames_add, classnames_exclude)
)
if('edit' not in exclude and ph.user_can_edit_obj(usr, obj)):
btns.append(
self.edit_button(pk, classnames_add, classnames_exclude)
)
if('delete' not in exclude and ph.user_can_delete_obj(usr, obj)):
btns.append(
self.delete_button(pk, classnames_add, classnames_exclude)
)
return btns
class PageButtonHelper(ButtonHelper):
unpublish_button_classnames = []
copy_button_classnames = []
def unpublish_button(self, pk, classnames_add=[], classnames_exclude=[]):
classnames = self.unpublish_button_classnames + classnames_add
cn = self.finalise_classname(classnames, classnames_exclude)
return {
'url': self.url_helper.get_action_url('unpublish', quote(pk)),
'label': _('Unpublish'),
'classname': cn,
'title': _('Unpublish this %s') % self.verbose_name,
}
def copy_button(self, pk, classnames_add=[], classnames_exclude=[]):
classnames = self.copy_button_classnames + classnames_add
cn = self.finalise_classname(classnames, classnames_exclude)
return {
'url': self.url_helper.get_action_url('copy', quote(pk)),
'label': _('Copy'),
'classname': cn,
'title': _('Copy this %s') % self.verbose_name,
}
def get_buttons_for_obj(self, obj, exclude=[], classnames_add=[],
classnames_exclude=[]):
ph = self.permission_helper
usr = self.request.user
pk = quote(getattr(obj, self.opts.pk.attname))
btns = []
if('inspect' not in exclude and ph.user_can_inspect_obj(usr, obj)):
btns.append(
self.inspect_button(pk, classnames_add, classnames_exclude)
)
if('edit' not in exclude and ph.user_can_edit_obj(usr, obj)):
btns.append(
self.edit_button(pk, classnames_add, classnames_exclude)
)
if('copy' not in exclude and ph.user_can_copy_obj(usr, obj)):
btns.append(
self.copy_button(pk, classnames_add, classnames_exclude)
)
if('unpublish' not in exclude and ph.user_can_unpublish_obj(usr, obj)):
btns.append(
self.unpublish_button(pk, classnames_add, classnames_exclude)
)
if('delete' not in exclude and ph.user_can_delete_obj(usr, obj)):
btns.append(
self.delete_button(pk, classnames_add, classnames_exclude)
)
return btns

Wyświetl plik

@ -0,0 +1,54 @@
from __future__ import absolute_import, unicode_literals
from wagtail.wagtailadmin.menu import Menu, MenuItem, SubmenuMenuItem
class ModelAdminMenuItem(MenuItem):
"""
A sub-class of wagtail's MenuItem, used by PageModelAdmin to add a link
to its listing page
"""
def __init__(self, model_admin, order):
self.model_admin = model_admin
url = model_admin.url_helper.index_url
classnames = 'icon icon-%s' % model_admin.get_menu_icon()
super(ModelAdminMenuItem, self).__init__(
label=model_admin.get_menu_label(), url=url,
classnames=classnames, order=order)
def is_shown(self, request):
return self.model_admin.permission_helper.user_can_list(request.user)
class GroupMenuItem(SubmenuMenuItem):
"""
A sub-class of wagtail's SubmenuMenuItem, used by ModelAdminGroup to add a
link to the admin menu with its own submenu, linking to various listing
pages
"""
def __init__(self, modeladmingroup, order, menu):
classnames = 'icon icon-%s' % modeladmingroup.get_menu_icon()
super(GroupMenuItem, self).__init__(
label=modeladmingroup.get_menu_label(), menu=menu,
classnames=classnames, order=order, )
def is_shown(self, request):
"""
If there aren't any visible items in the submenu, don't bother to show
this menu item
"""
for menuitem in self.menu._registered_menu_items:
if menuitem.is_shown(request):
return True
return False
class SubMenu(Menu):
"""
A sub-class of wagtail's Menu, used by AppModelAdmin. We just want to
override __init__, so that we can specify the items to include on
initialisation
"""
def __init__(self, menuitem_list):
self._registered_menu_items = menuitem_list
self.construct_hook_name = None

Wyświetl plik

@ -0,0 +1,588 @@
from __future__ import absolute_import, unicode_literals
from django.conf.urls import url
from django.contrib.auth.models import Permission
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Model
from django.forms.widgets import flatatt
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page
from wagtail.wagtailimages.models import Filter
from .helpers import (
AdminURLHelper, ButtonHelper, PageAdminURLHelper, PageButtonHelper, PagePermissionHelper,
PermissionHelper)
from .menus import GroupMenuItem, ModelAdminMenuItem, SubMenu
from .views import ChooseParentView, CreateView, DeleteView, EditView, IndexView, InspectView
class WagtailRegisterable(object):
"""
Base class, providing a more convenient way for ModelAdmin or
ModelAdminGroup instances to be registered with Wagtail's admin area.
"""
add_to_settings_menu = False
def register_with_wagtail(self):
@hooks.register('register_permissions')
def register_permissions():
return self.get_permissions_for_registration()
@hooks.register('register_admin_urls')
def register_admin_urls():
return self.get_admin_urls_for_registration()
menu_hook = (
'register_settings_menu_item' if self.add_to_settings_menu else
'register_admin_menu_item'
)
@hooks.register(menu_hook)
def register_admin_menu_item():
return self.get_menu_item()
class ThumbnailMixin(object):
"""
Mixin class to help display thumbnail images in ModelAdmin listing results.
`thumb_image_field_name` must be overridden to name a ForeignKey field on
your model, linking to `wagtailimages.Image`.
"""
thumb_image_field_name = 'image'
thumb_image_filter_spec = 'fill-100x100'
thumb_image_width = 50
thumb_classname = 'admin-thumb'
thumb_col_header_text = _('image')
thumb_default = None
def admin_thumb(self, obj):
try:
image = getattr(obj, self.thumb_image_field_name, None)
except AttributeError:
raise ImproperlyConfigured(
u"The `thumb_image_field_name` attribute on your `%s` class "
"must name a field on your model." % self.__class__.__name__
)
img_attrs = {
'src': self.thumb_default,
'width': self.thumb_image_width,
'class': self.thumb_classname,
}
if image:
fltr, _ = Filter.objects.get_or_create(
spec=self.thumb_image_filter_spec)
img_attrs.update({'src': image.get_rendition(fltr).url})
return mark_safe('<img{}>'.format(flatatt(img_attrs)))
elif self.thumb_default:
return mark_safe('<img{}>'.format(flatatt(img_attrs)))
return ''
admin_thumb.short_description = thumb_col_header_text
class ModelAdmin(WagtailRegisterable):
"""
The core modeladmin class. It provides an alternative means to
list and manage instances of a given 'model' within Wagtail's admin area.
It is essentially comprised of attributes and methods that allow a degree
of control over how the data is represented, and other methods to make the
additional functionality available via various Wagtail hooks.
"""
model = None
menu_label = None
menu_icon = None
menu_order = None
list_display = ('__str__',)
list_display_add_buttons = None
inspect_view_fields = []
inspect_view_fields_exclude = []
inspect_view_enabled = False
empty_value_display = '-'
list_filter = ()
list_select_related = False
list_per_page = 100
search_fields = None
ordering = None
parent = None
index_view_class = IndexView
create_view_class = CreateView
edit_view_class = EditView
inspect_view_class = InspectView
delete_view_class = DeleteView
choose_parent_view_class = ChooseParentView
index_template_name = ''
create_template_name = ''
edit_template_name = ''
inspect_template_name = ''
delete_template_name = ''
choose_parent_template_name = ''
permission_helper_class = None
url_helper_class = None
button_helper_class = None
index_view_extra_css = []
index_view_extra_js = []
inspect_view_extra_css = []
inspect_view_extra_js = []
form_view_extra_css = []
form_view_extra_js = []
def __init__(self, parent=None):
"""
Don't allow initialisation unless self.model is set to a valid model
"""
if not self.model or not issubclass(self.model, Model):
raise ImproperlyConfigured(
u"The model attribute on your '%s' class must be set, and "
"must be a valid Django model." % self.__class__.__name__)
self.opts = self.model._meta
self.is_pagemodel = issubclass(self.model, Page)
self.parent = parent
self.permission_helper = self.get_permission_helper_class()(
self.model, self.inspect_view_enabled)
self.url_helper = self.get_url_helper_class()(self.model)
def get_permission_helper_class(self):
"""
Returns a permission_helper class to help with permission-based logic
for the given model.
"""
if self.permission_helper_class:
return self.permission_helper_class
if self.is_pagemodel:
return PagePermissionHelper
return PermissionHelper
def get_url_helper_class(self):
if self.url_helper_class:
return self.url_helper_class
if self.is_pagemodel:
return PageAdminURLHelper
return AdminURLHelper
def get_button_helper_class(self):
"""
Returns a ButtonHelper class to help generate buttons for the given
model.
"""
if self.button_helper_class:
return self.button_helper_class
if self.is_pagemodel:
return PageButtonHelper
return ButtonHelper
def get_menu_label(self):
"""
Returns the label text to be used for the menu item.
"""
return self.menu_label or self.opts.verbose_name_plural.title()
def get_menu_icon(self):
"""
Returns the icon to be used for the menu item. The value is prepended
with 'icon-' to create the full icon class name. For design
consistency, the same icon is also applied to the main heading for
views called by this class.
"""
if self.menu_icon:
return self.menu_icon
if self.is_pagemodel:
return 'doc-full-inverse'
return 'snippet'
def get_menu_order(self):
"""
Returns the 'order' to be applied to the menu item. 000 being first
place. Where ModelAdminGroup is used, the menu_order value should be
applied to that, and any ModelAdmin classes added to 'items'
attribute will be ordered automatically, based on their order in that
sequence.
"""
return self.menu_order or 999
def get_list_display(self, request):
"""
Return a sequence containing the fields/method output to be displayed
in the list view.
"""
return self.list_display
def get_list_display_add_buttons(self, request):
"""
Return the name of the field/method from list_display where action
buttons should be added. Defaults to the first item from
get_list_display()
"""
return self.list_display_add_buttons or self.get_list_display(
request)[0]
def get_empty_value_display(self, field_name=None):
"""
Return the empty_value_display value defined on ModelAdmin
"""
return mark_safe(self.empty_value_display)
def get_list_filter(self, request):
"""
Returns a sequence containing the fields to be displayed as filters in
the right sidebar in the list view.
"""
return self.list_filter
def get_ordering(self, request):
"""
Returns a sequence defining the default ordering for results in the
list view.
"""
return self.ordering or ()
def get_queryset(self, request):
"""
Returns a QuerySet of all model instances that can be edited by the
admin site.
"""
qs = self.model._default_manager.get_queryset()
ordering = self.get_ordering(request)
if ordering:
qs = qs.order_by(*ordering)
return qs
def get_search_fields(self, request):
"""
Returns a sequence defining which fields on a model should be searched
when a search is initiated from the list view.
"""
return self.search_fields or ()
def get_extra_class_names_for_field_col(self, obj, field_name):
"""
Return a list of additional CSS class names to be added to the table
cell's `class` attribute when rendering the output of `field_name` for
`obj` in `index_view`.
Must always return a list.
"""
return []
def get_extra_attrs_for_field_col(self, obj, field_name):
"""
Return a dictionary of additional HTML attributes to be added to a
table cell when rendering the output of `field_name` for `obj` in
`index_view`.
Must always return a dictionary.
"""
return {}
def get_index_view_extra_css(self):
css = ['wagtailmodeladmin/css/index.css']
css.extend(self.index_view_extra_css)
return css
def get_index_view_extra_js(self):
return self.index_view_extra_js
def get_form_view_extra_css(self):
return self.form_view_extra_css
def get_form_view_extra_js(self):
return self.form_view_extra_js
def get_inspect_view_extra_css(self):
return self.inspect_view_extra_css
def get_inspect_view_extra_js(self):
return self.inspect_view_extra_js
def get_inspect_view_fields(self):
"""
Return a list of field names, indicating the model fields that
should be displayed in the 'inspect' view. Returns the value of the
'inspect_view_fields' attribute if populated, otherwise a sensible
list of fields is generated automatically, with any field named in
'inspect_view_fields_exclude' not being included.
"""
if not self.inspect_view_fields:
found_fields = []
for f in self.model._meta.get_fields():
if f.name not in self.inspect_view_fields_exclude:
if f.concrete and (
not f.is_relation or
(not f.auto_created and f.related_model)
):
found_fields.append(f.name)
return found_fields
return self.inspect_view_fields
def index_view(self, request):
"""
Instantiates a class-based view to provide listing functionality for
the assigned model. The view class used can be overridden by changing
the 'index_view_class' attribute.
"""
kwargs = {'model_admin': self}
view_class = self.index_view_class
return view_class.as_view(**kwargs)(request)
def create_view(self, request):
"""
Instantiates a class-based view to provide 'creation' functionality for
the assigned model, or redirect to Wagtail's create view if the
assigned model extends 'Page'. The view class used can be overridden by
changing the 'create_view_class' attribute.
"""
kwargs = {'model_admin': self}
view_class = self.create_view_class
return view_class.as_view(**kwargs)(request)
def choose_parent_view(self, request):
"""
Instantiates a class-based view to allows a parent page to be chosen
for a new object, where the assigned model extends Wagtail's Page
model, and there is more than one potential parent for new instances.
The view class used can be overridden by changing the
'choose_parent_view_class' attribute.
"""
kwargs = {'model_admin': self}
view_class = self.choose_parent_view_class
return view_class.as_view(**kwargs)(request)
def inspect_view(self, request, instance_pk):
"""
Instantiates a class-based view to provide 'inspect' functionality for
the assigned model. The view class used can be overridden by changing
the 'inspect_view_class' attribute.
"""
kwargs = {'model_admin': self, 'instance_pk': instance_pk}
view_class = self.inspect_view_class
return view_class.as_view(**kwargs)(request)
def edit_view(self, request, instance_pk):
"""
Instantiates a class-based view to provide 'edit' functionality for the
assigned model, or redirect to Wagtail's edit view if the assinged
model extends 'Page'. The view class used can be overridden by changing
the 'edit_view_class' attribute.
"""
kwargs = {'model_admin': self, 'instance_pk': instance_pk}
view_class = self.edit_view_class
return view_class.as_view(**kwargs)(request)
def delete_view(self, request, instance_pk):
"""
Instantiates a class-based view to provide 'delete confirmation'
functionality for the assigned model, or redirect to Wagtail's delete
confirmation view if the assinged model extends 'Page'. The view class
used can be overridden by changing the 'delete_view_class'
attribute.
"""
kwargs = {'model_admin': self, 'instance_pk': instance_pk}
view_class = self.delete_view_class
return view_class.as_view(**kwargs)(request)
def get_templates(self, action='index'):
"""
Utility funtion that provides a list of templates to try for a given
view, when the template isn't overridden by one of the template
attributes on the class.
"""
app_label = self.opts.app_label.lower()
model_name = self.opts.model_name.lower()
return [
'modeladmin/%s/%s/%s.html' % (app_label, model_name, action),
'modeladmin/%s/%s.html' % (app_label, action),
'modeladmin/%s.html' % (action,),
]
def get_index_template(self):
"""
Returns a template to be used when rendering 'index_view'. If a
template is specified by the 'index_template_name' attribute, that will
be used. Otherwise, a list of preferred template names are returned.
"""
return self.index_template_name or self.get_templates('index')
def get_choose_parent_template(self):
"""
Returns a template to be used when rendering 'choose_parent_view'. If a
template is specified by the 'choose_parent_template_name' attribute,
that will be used. Otherwise, a list of preferred template names are
returned.
"""
return self.choose_parent_template_name or self.get_templates(
'choose_parent')
def get_inspect_template(self):
"""
Returns a template to be used when rendering 'inspect_view'. If a
template is specified by the 'inspect_template_name' attribute, that
will be used. Otherwise, a list of preferred template names are
returned.
"""
return self.inspect_template_name or self.get_templates('inspect')
def get_create_template(self):
"""
Returns a template to be used when rendering 'create_view'. If a
template is specified by the 'create_template_name' attribute,
that will be used. Otherwise, a list of preferred template names are
returned.
"""
return self.create_template_name or self.get_templates('create')
def get_edit_template(self):
"""
Returns a template to be used when rendering 'edit_view'. If a template
is specified by the 'edit_template_name' attribute, that will be used.
Otherwise, a list of preferred template names are returned.
"""
return self.edit_template_name or self.get_templates('edit')
def get_delete_template(self):
"""
Returns a template to be used when rendering 'delete_view'. If
a template is specified by the 'delete_template_name'
attribute, that will be used. Otherwise, a list of preferred template
names are returned.
"""
return self.delete_template_name or self.get_templates('delete')
def get_menu_item(self, order=None):
"""
Utilised by Wagtail's 'register_menu_item' hook to create a menu item
to access the listing view, or can be called by ModelAdminGroup
to create a SubMenu
"""
return ModelAdminMenuItem(self, order or self.get_menu_order())
def get_permissions_for_registration(self):
"""
Utilised by Wagtail's 'register_permissions' hook to allow permissions
for a model to be assigned to groups in settings. This is only required
if the model isn't a Page model, and isn't registered as a Snippet
"""
from wagtail.wagtailsnippets.models import SNIPPET_MODELS
if not self.is_pagemodel and self.model not in SNIPPET_MODELS:
return self.permission_helper.get_all_model_permissions()
return Permission.objects.none()
def get_admin_urls_for_registration(self):
"""
Utilised by Wagtail's 'register_admin_urls' hook to register urls for
our the views that class offers.
"""
urls = (
url(self.url_helper.get_action_url_pattern('index'),
self.index_view,
name=self.url_helper.get_action_url_name('index')),
url(self.url_helper.get_action_url_pattern('create'),
self.create_view,
name=self.url_helper.get_action_url_name('create')),
url(self.url_helper.get_action_url_pattern('edit'),
self.edit_view,
name=self.url_helper.get_action_url_name('edit')),
url(self.url_helper.get_action_url_pattern('delete'),
self.delete_view,
name=self.url_helper.get_action_url_name('delete')),
)
if self.inspect_view_enabled:
urls = urls + (
url(self.url_helper.get_action_url_pattern('inspect'),
self.inspect_view,
name=self.url_helper.get_action_url_name('inspect')),
)
if self.is_pagemodel:
urls = urls + (
url(self.url_helper.get_action_url_pattern('choose_parent'),
self.choose_parent_view,
name=self.url_helper.get_action_url_name('choose_parent')),
)
return urls
class ModelAdminGroup(WagtailRegisterable):
"""
Acts as a container for grouping together mutltiple PageModelAdmin and
SnippetModelAdmin instances. Creates a menu item with a SubMenu for
accessing the listing pages of those instances
"""
items = ()
menu_label = None
menu_order = None
menu_icon = None
def __init__(self):
"""
When initialising, instantiate the classes within 'items', and assign
the instances to a 'modeladmin_instances' attribute for convienient
access later
"""
self.modeladmin_instances = []
for ModelAdminClass in self.items:
self.modeladmin_instances.append(ModelAdminClass(parent=self))
def get_menu_label(self):
return self.menu_label or self.get_app_label_from_subitems()
def get_app_label_from_subitems(self):
for instance in self.modeladmin_instances:
return instance.opts.app_label.title()
return ''
def get_menu_icon(self):
return self.menu_icon or 'icon-folder-open-inverse'
def get_menu_order(self):
return self.menu_order or 999
def get_menu_item(self):
"""
Utilised by Wagtail's 'register_menu_item' hook to create a menu
for this group with a SubMenu linking to listing pages for any
associated ModelAdmin instances
"""
if self.modeladmin_instances:
submenu = SubMenu(self.get_submenu_items())
return GroupMenuItem(self, self.get_menu_order(), submenu)
def get_submenu_items(self):
menu_items = []
item_order = 1
for modeladmin in self.modeladmin_instances:
menu_items.append(modeladmin.get_menu_item(order=item_order))
item_order += 1
return menu_items
def get_permissions_for_registration(self):
"""
Utilised by Wagtail's 'register_permissions' hook to allow permissions
for a all models grouped by this class to be assigned to Groups in
settings.
"""
qs = Permission.objects.none()
for instance in self.modeladmin_instances:
qs = qs | instance.get_permissions_for_registration()
return qs
def get_admin_urls_for_registration(self):
"""
Utilised by Wagtail's 'register_admin_urls' hook to register urls for
used by any associated ModelAdmin instances
"""
urls = tuple()
for instance in self.modeladmin_instances:
urls += instance.get_admin_urls_for_registration()
return urls
def modeladmin_register(modeladmin_class):
"""
Method for registering ModelAdmin or ModelAdminGroup classes with Wagtail.
"""
instance = modeladmin_class()
instance.register_with_wagtail()

Wyświetl plik

@ -0,0 +1,6 @@
#id_parent_page li {
margin: 15px 0;
}
#id_parent_page li label {
float: none;
}

Wyświetl plik

@ -0,0 +1,147 @@
.content header { margin-bottom: 0; }
#result_list {
padding: 0 15px;
}
#result_list table { margin-bottom:0; }
#result_list tbody th {
background-color: transparent;
text-align: left;
padding: 1.2em 1em;
}
#result_list tbody tr:hover ul.actions {
visibility: visible;
}
#result_list tbody td, #result_list tbody th {
vertical-align: top;
}
#changelist-filter {
padding: 0 15px;
}
#changelist-filter h2 {
background-color: #fafafa;
font-size: 13px;
line-height: 31px;
margin-top: 0;
padding-left: 8px;
border-bottom: 1px solid #E6E6E6;
}
#changelist-filter h3 {
font-size: 12px;
margin-bottom: 0;
}
#changelist-filter ul {
padding-left: 0;
margin-bottom: 25px;
}
#changelist-filter li {
list-style-type: none;
margin: 0 0 4px;
padding-left: 0;
}
#changelist-filter a {
font-family: Open Sans,Arial,sans-serif;
border-radius: 3px;
width: auto;
line-height: 1.2em;
padding: 8px 12px;
font-size: 0.9em;
font-weight: normal;
vertical-align: middle;
display: block;
background-color: white;
border: 1px solid #43b1b0;
color: #43b1b0;
text-decoration: none;
text-transform: uppercase;
position: relative;
overflow: hidden;
outline: none;
box-sizing: border-box;
-webkit-font-smoothing: auto;
-moz-appearance: none;
-moz-box-sizing: border-box;
}
#changelist-filter a:hover {
background-color: #358c8b;
border-color: #358c8b;
color: white;
}
#changelist-filter li.selected a {
color: white !important;
border-color: #43b1b0 !important;
background-color: #43b1b0;
}
.no-search-results {
margin-top: 30px;
}
.no-search-results h2 {
padding-top: 0.3em;
margin-bottom: 0.3em;
}
.no-search-results img {
float: left;
margin: 0 15px 15px 0;
width: 50px;
}
div.pagination {
margin-top: 3em;
border-top: 1px dashed #d9d9d9;
padding: 2em 1em 0;
}
div.pagination ul {
margin-top: -1.25em
}
p.no-results {
margin: 30px 1em 0;
}
@media screen and (min-width: 50em) {
#changelist-filter {
float: right;
padding: 0 1.5%;
}
#result_list {
padding: 0 1.5% 0 0;
}
#result_list tbody th:first-child {
padding-left: 50px;
}
#result_list.col12 tbody td:last-child {
padding-right: 50px;
}
div.pagination {
padding-left: 50px;
padding-right: 50px;
}
div.pagination.col9 {
width: 73.5%;
}
p.no-results {
margin: 30px 50px 0;
}
}
@media screen and (min-width: 1200px) {
#result_list.col9 {
width: 79%;
}
#changelist-filter {
width: 21%;
}
div.pagination.col9 {
width: 77.5%;
}
}

Wyświetl plik

@ -0,0 +1,43 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n admin_static %}
{% block titletag %}{{ view.get_meta_title }}{% endblock %}
{% block extra_css %}
{% include "wagtailadmin/pages/_editor_css.html" %}
<link rel="stylesheet" href="{% static 'modeladmin/css/choose_parent_page.css' %}" type="text/css"/>
{% endblock %}
{% block extra_js %}
{% include "wagtailadmin/pages/_editor_js.html" %}
{% endblock %}
{% block content %}
<div id="content-main">
{% block header %}
{% include "modeladmin/includes/breadcrumb.html" %}
{% include "wagtailadmin/shared/header.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon %}
{% endblock %}
<div class="nice-padding">
<h2>{% blocktrans %}Choose a parent page{% endblocktrans %}</h2>
<p>{% blocktrans with view.verbose_name_plural|capfirst as plural %}{{ plural }} can be added to more than one place within your site. Which of the following would you like to be the parent of your new page?{% endblocktrans %}</p>
<form action="" method="post">
{% csrf_token %}
<ul class="fields">
{% include "wagtailadmin/shared/field_as_li.html" with field=form.parent_page %}
<li>
<input type="submit" class="button" value="{% trans 'Continue' %}">
</li>
</ul>
</form>
</div>
</div>
{% endblock %}

Wyświetl plik

@ -0,0 +1,41 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{{ view.get_meta_title }}{% endblock %}
{% block extra_css %}
{% include "wagtailadmin/pages/_editor_css.html" %}
{{ view.media.css }}
{% endblock %}
{% block extra_js %}
{% include "wagtailadmin/pages/_editor_js.html" %}
{{ view.media.js }}
{% endblock %}
{% block content %}
{% block header %}
{% include "wagtailadmin/shared/header.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=1 merged=1 %}
{% endblock %}
<form action="{% block form_action %}{{ view.create_url }}{% endblock %}"{% if is_multipart %} enctype="multipart/form-data"{% endif %} method="POST">
{% csrf_token %}
{% block form %}{{ edit_handler.render_form_content }}{% endblock %}
{% block footer %}
<footer>
<ul>
<li class="actions">
{% block form_actions %}
<div class="dropdown dropup match-width">
<div><input type="submit" value="{% trans 'Save' %}" /></div>
</div>
{% endblock %}
</li>
</ul>
</footer>
{% endblock %}
</form>
{% endblock %}

Wyświetl plik

@ -0,0 +1,30 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n modeladmin_tags %}
{% block titletag %}{{ view.get_meta_title }}{% endblock %}
{% block content %}
{% block header %}
{% include "wagtailadmin/shared/header.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon %}
{% endblock %}
{% block content_main %}
<div class="nice-padding">
{% if protected_error %}
<h2>{% blocktrans with view.verbose_name|capfirst as model_name %}{{ model_name }} could not be deleted{% endblocktrans %}</h2>
<p>{% blocktrans with instance as instance_name %}'{{ instance_name }}' is currently referenced by other objects, and cannot be deleted without jeopardising data integrity. To delete it successfully, first remove references from the following objects, then try again:{% endblocktrans %}</p>
<ul>
{% for obj in linked_objects %}<li><b>{{ obj|get_content_type_for_obj|title }}:</b> {{ obj }}</li>{% endfor %}
</ul>
<p><a href="{{ view.index_url }}" class="button">{% trans 'Go back to listing' %}</a></p>
{% else %}
<p>{{ view.confirmation_message }}</p>
<form action="{{ view.delete_url }}" method="POST">
{% csrf_token %}
<input type="submit" value="{% trans 'Yes, delete' %}" class="serious" />
</form>
{% endif %}
</div>
{% endblock %}
{% endblock %}

Wyświetl plik

@ -0,0 +1,18 @@
{% extends "modeladmin/create.html" %}
{% load i18n %}
{% block form_action %}{{ view.edit_url }}{% endblock %}
{% block form_actions %}
<div class="dropdown dropup match-width">
<div><input type="submit" value="{% trans 'Save' %}" /></div>
{% if user_can_delete %}
<div class="dropdown-toggle icon icon-arrow-up"></div>
<ul role="menu">
<li><a href="{{ view.delete_url }}" class="shortcut">{% trans "Delete" %}</a></li>
</ul>
{% endif %}
</div>
{% endblock %}

Wyświetl plik

@ -0,0 +1,5 @@
{% load i18n %}
<ul class="breadcrumb">
<li class="home"><a href="{% url 'wagtailadmin_home' %}" class="icon icon-home text-replace">{% trans 'Home' %}</a></li>
<li><a href="{{ view.index_url }}">{{ view.verbose_name_plural|capfirst }}</a></li>
</ul>

Wyświetl plik

@ -0,0 +1 @@
<a{% if button.url %} href="{{ button.url }}"{% endif %} class="{{ button.classname }}" title="{{ button.title }}"{% if button.target %} target="{{ button.target }}"{% endif %}>{{ button.label }}</a>

Wyświetl plik

@ -0,0 +1,8 @@
{% load i18n %}
{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}
<ul>
{% for choice in choices %}
<li{% if choice.selected %} class="selected"{% endif %}>
<a href="{{ choice.query_string|iriencode }}">{{ choice.display }}</a></li>
{% endfor %}
</ul>

Wyświetl plik

@ -0,0 +1,28 @@
{% load i18n modeladmin_tags %}
{% if results %}
<table class="listing full-width">
<thead>
<tr>
{% for header in result_headers %}
<th scope="col" {{ header.class_attrib }}>
{% if header.sortable %}<a href="{{ header.url_primary }}" class="teal icon {% if header.ascending %}icon-arrow-up-after{% else %}icon-arrow-down-after{% endif %}">{% endif %}
{{ header.text|capfirst }}
{% if header.sortable %}</a>{% endif %}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for result in results %}
<tr class="{% cycle 'odd' 'even' %}">
{% result_row_display forloop.counter0 %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="nice-padding no-search-results">
<p>{% blocktrans with view.verbose_name_plural as name %}Sorry, there are no {{ name }} matching your search parameters.{% endblocktrans %}</p>
</div>
{% endif %}

Wyświetl plik

@ -0,0 +1,4 @@
{% load modeladmin_tags %}
{% for item in result %}
{% result_row_value_display forloop.counter0 %}
{% endfor %}

Wyświetl plik

@ -0,0 +1,11 @@
{% load i18n modeladmin_tags %}
{{ item }}{% if add_action_buttons %}
{% if action_buttons %}
<ul class="actions">
{% for button in action_buttons %}
<li>{% include 'modeladmin/includes/button.html' %}</li>
{% endfor %}
</ul>
{% endif %}
{{ item_closing_tag }}
{% endif %}

Wyświetl plik

@ -0,0 +1,20 @@
{% load i18n admin_static %}
{% if view.search_fields %}
<form id="changelist-search" class="col search-form" action="{{ view.index_url }}" method="get">
<ul class="fields">
<li class="required">
<div class="field char_field text_input field-small iconfield">
<label for="id_q" class="visuallyhidden">{% trans 'Search for' %}</label>
<div class="field-content">
<div class="input icon-search ">
<input id="id_q" name="{{ search_var }}" value="{{ view.query }}" placeholder="{% blocktrans with view.model_name_plural|lower as name %}Search {{ name }}{% endblocktrans %}" type="text">
<span></span>
</div>
</div>
</div>
</li>
<li class="submit visuallyhidden"><input type="submit" value="Search" class="button"></li>
</ul>
</form>
<script type="text/javascript">document.getElementById("id_q").focus();</script>
{% endif %}

Wyświetl plik

@ -0,0 +1,93 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n modeladmin_tags %}
{% block titletag %}{{ view.get_meta_title }}{% endblock %}
{% block css %}
{{ block.super }}
{{ view.media.css }}
{% endblock %}
{% block extra_js %}
{{ view.media.js }}
{% endblock %}
{% block content %}
{% block header %}
<header class="nice-padding hasform">
<div class="row">
<div class="left">
<div class="col">
{% block h1 %}<h1 {% if view.header_icon %}class="icon icon-{{ view.header_icon }}"{% endif %}>{{ view.get_page_title }}<span></span></h1>{% endblock %}
</div>
{% block search %}{% search_form %}{% endblock %}
</div>
{% block header_extra %}
{% if user_can_create %}
<div class="right">
<div class="addbutton">
{% include 'modeladmin/includes/button.html' with button=view.button_helper.add_button %}
</div>
</div>
{% endif %}
{% endblock %}
</div>
</header>
{% endblock %}
{% block content_main %}
<div id="content-main">
<div class="row" id="changelist">
{% block content_cols %}
{% block filters %}
{% if view.has_filters and all_count %}
<div id="changelist-filter" class="col3">
<h2>{% trans 'Filter' %}</h2>
{% for spec in view.filter_specs %}{% admin_list_filter view spec %}{% endfor %}
</div>
{% endif %}
{% endblock %}
<div id="result_list" class="{% if view.has_filters and all_count %}col9{% else %}col12{% endif %}">
{% block result_list %}
{% if not all_count %}
<div class="nice-padding" style="margin-top:30px;">
{% if no_valid_parents %}
<p>{% blocktrans with view.verbose_name_plural as name %}No {{ name }} have been created yet. One of the following must be created before you can add any {{ name }}:{% endblocktrans %}</p>
<ul>
{% for type in required_parent_types %}<li><b>{{ type|title }}</b></li>{% endfor %}
</ul>
{% else %}
<p>{% blocktrans with view.verbose_name_plural as name %}No {{ name }} have been created yet.{% endblocktrans %}
{% if user_can_create %}
{% blocktrans with view.create_url as url %}
Why not <a href="{{ url }}">add one</a>?
{% endblocktrans %}
{% endif %}</p>
{% endif %}
</div>
{% else %}
{% result_list %}
{% endif %}
{% endblock %}
</div>
{% block pagination %}
<div class="pagination {% if view.has_filters and all_count %}col9{% else %}col12{% endif %}">
<p>{% blocktrans with page_obj.number as current_page and paginator.num_pages as num_pages %}Page {{ current_page }} of {{ num_pages }}.{% endblocktrans %}</p>
{% if paginator.num_pages > 1 %}
<ul>
{% pagination_link_previous page_obj view %}
{% pagination_link_next page_obj view %}
</ul>
{% endif %}
</div>
{% endblock %}
{% endblock %}
</div>
</div>
{% endblock %}
{% endblock %}

Wyświetl plik

@ -0,0 +1,55 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{{ view.get_meta_title }}{% endblock %}
{% block extra_css %}
{{ view.media.css }}
{% endblock %}
{% block extra_js %}
{{ view.media.js }}
{% endblock %}
{% block content %}
<div id="content-main">
{% block header %}
{% include "modeladmin/includes/breadcrumb.html" %}
{% include "wagtailadmin/shared/header.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon %}
{% endblock %}
{% block content_main %}
<div class="nice-padding">
<p class="back"><a href="{{ view.index_url }}" class="icon icon-arrow-left">{% blocktrans with view.verbose_name as model_name %}Back to {{ model_name }} list{% endblocktrans %}</a></p>
{% block fields_output %}
{% if fields %}
<dl>
{% for field in fields %}
<dt class="{{ field.type|lower }}">{{ field.label }}</dt>
<dd>{{ field.value }}</dd>
{% endfor %}
</dl>
{% endif %}
{% endblock %}
</div>
{% endblock %}
</div>
{% block footer %}
{% if buttons %}
<footer>
<div class="button-group">
{% for button in buttons %}
{% include "modeladmin/includes/button.html" %}
{% endfor %}
</div>
</footer>
{% endif %}
{% endblock %}
{% endblock %}

Wyświetl plik

@ -0,0 +1,188 @@
from __future__ import absolute_import, unicode_literals
import datetime
import django
from django.contrib.admin.templatetags.admin_list import ResultList, result_headers
from django.contrib.admin.utils import display_for_field, display_for_value, lookup_field
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.template import Library
from django.template.loader import get_template
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
register = Library()
def items_for_result(view, result):
"""
Generates the actual list of data.
"""
modeladmin = view.model_admin
for field_name in view.list_display:
empty_value_display = modeladmin.get_empty_value_display(field_name)
row_classes = ['field-%s' % field_name]
try:
f, attr, value = lookup_field(field_name, result, modeladmin)
except ObjectDoesNotExist:
result_repr = empty_value_display
else:
empty_value_display = getattr(
attr, 'empty_value_display', empty_value_display)
if f is None or f.auto_created:
allow_tags = getattr(attr, 'allow_tags', False)
boolean = getattr(attr, 'boolean', False)
if boolean or not value:
allow_tags = True
if django.VERSION >= (1, 9):
result_repr = display_for_value(
value, empty_value_display, boolean)
else:
result_repr = display_for_value(value, boolean)
# Strip HTML tags in the resulting text, except if the
# function has an "allow_tags" attribute set to True.
if allow_tags:
result_repr = mark_safe(result_repr)
if isinstance(value, (datetime.date, datetime.time)):
row_classes.append('nowrap')
else:
if isinstance(f, models.ManyToOneRel):
field_val = getattr(result, f.name)
if field_val is None:
result_repr = empty_value_display
else:
result_repr = field_val
else:
if django.VERSION >= (1, 9):
result_repr = display_for_field(
value, f, empty_value_display)
else:
result_repr = display_for_field(value, f)
if isinstance(f, (
models.DateField, models.TimeField, models.ForeignKey)
):
row_classes.append('nowrap')
if force_text(result_repr) == '':
result_repr = mark_safe('&nbsp;')
row_classes.extend(
modeladmin.get_extra_class_names_for_field_col(field_name, result))
row_attrs_dict = modeladmin.get_extra_attrs_for_field_col(
field_name, result)
row_attrs_dict['class'] = ' ' . join(row_classes)
row_attrs = ''.join(
' %s="%s"' % (key, val) for key, val in row_attrs_dict.items())
row_attrs_safe = mark_safe(row_attrs)
yield format_html('<td{}>{}</td>', row_attrs_safe, result_repr)
def results(view, object_list):
for item in object_list:
yield ResultList(None, items_for_result(view, item))
@register.inclusion_tag("modeladmin/includes/result_list.html",
takes_context=True)
def result_list(context):
"""
Displays the headers and data list together
"""
view = context['view']
object_list = context['object_list']
headers = list(result_headers(view))
num_sorted_fields = 0
for h in headers:
if h['sortable'] and h['sorted']:
num_sorted_fields += 1
context.update({
'result_headers': headers,
'num_sorted_fields': num_sorted_fields,
'results': list(results(view, object_list))})
return context
@register.simple_tag
def pagination_link_previous(current_page, view):
if current_page.has_previous():
previous_page_number0 = current_page.previous_page_number() - 1
return format_html(
'<li class="prev"><a href="%s" class="icon icon-arrow-left">%s'
'</a></li>' %
(view.get_query_string({view.PAGE_VAR: previous_page_number0}),
_('Previous'))
)
return ''
@register.simple_tag
def pagination_link_next(current_page, view):
if current_page.has_next():
next_page_number0 = current_page.next_page_number() - 1
return format_html(
'<li class="next"><a href="%s" class="icon icon-arrow-right-after"'
'>%s</a></li>' %
(view.get_query_string({view.PAGE_VAR: next_page_number0}),
_('Next'))
)
return ''
@register.inclusion_tag(
"modeladmin/includes/search_form.html", takes_context=True)
def search_form(context):
context.update({'search_var': context['view'].SEARCH_VAR})
return context
@register.simple_tag
def admin_list_filter(view, spec):
template_name = spec.template
if template_name == 'admin/filter.html':
template_name = 'modeladmin/includes/filter.html'
tpl = get_template(template_name)
return tpl.render({
'title': spec.title,
'choices': list(spec.choices(view)),
'spec': spec,
})
@register.inclusion_tag(
"modeladmin/includes/result_row.html", takes_context=True)
def result_row_display(context, index):
obj = context['object_list'][index]
view = context['view']
context.update({
'obj': obj,
'action_buttons': view.get_buttons_for_obj(obj),
})
return context
@register.inclusion_tag(
"modeladmin/includes/result_row_value.html", takes_context=True)
def result_row_value_display(context, index):
add_action_buttons = False
item = context['item']
closing_tag = mark_safe(item[-5:])
request = context['request']
model_admin = context['view'].model_admin
field_name = model_admin.get_list_display(request)[index]
if field_name == model_admin.get_list_display_add_buttons(request):
add_action_buttons = True
item = mark_safe(item[0:-5])
context.update({
'item': item,
'add_action_buttons': add_action_buttons,
'closing_tag': closing_tag,
})
return context
@register.filter
def get_content_type_for_obj(obj):
return obj.__class__._meta.verbose_name

Wyświetl plik

@ -0,0 +1,221 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.test import TestCase
from wagtail.wagtailcore.models import Page
from wagtail.tests.testapp.models import BusinessIndex
from wagtail.tests.utils import WagtailTestUtils
class TestIndexView(TestCase, WagtailTestUtils):
fixtures = ['test_specific.json']
def setUp(self):
self.login()
def get(self, **params):
return self.client.get('/admin/tests/eventpage/', params)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
# There are four event pages in the test data
self.assertEqual(response.context['result_count'], 4)
# User has add permission
self.assertEqual(response.context['user_can_create'], True)
def test_filter(self):
# Filter by audience
response = self.get(audience__exact='public')
self.assertEqual(response.status_code, 200)
# Only three of the event page in the test data are 'public'
self.assertEqual(response.context['result_count'], 3)
for eventpage in response.context['object_list']:
self.assertEqual(eventpage.audience, 'public')
def test_search(self):
response = self.get(q='Someone')
self.assertEqual(response.status_code, 200)
# There are two eventpage's where the title contains 'Someone'
self.assertEqual(response.context['result_count'], 1)
def test_ordering(self):
response = self.get(o='0.1')
self.assertEqual(response.status_code, 200)
# There should still be four results
self.assertEqual(response.context['result_count'], 4)
class TestCreateView(TestCase, WagtailTestUtils):
fixtures = ['test_specific.json']
def setUp(self):
self.login()
def test_redirect_to_choose_parent(self):
# When more than one possible parent page exists, redirect to choose_parent
response = self.client.get('/admin/tests/eventpage/create/')
self.assertRedirects(response, '/admin/tests/eventpage/choose_parent/')
def test_one_parent_exists(self):
# Create a BusinessIndex page that BusinessChild can exist under
homepage = Page.objects.get(url_path='/home/')
business_index = BusinessIndex(title='Business Index')
homepage.add_child(instance=business_index)
# When one possible parent page exists, redirect straight to the page create view
response = self.client.get('/admin/tests/businesschild/create/')
expected_path = '/admin/pages/add/tests/businesschild/%d/' % business_index.pk
expected_next_path = '/admin/tests/businesschild/'
self.assertRedirects(response, '%s?next=%s' % (expected_path, expected_next_path))
class TestInspectView(TestCase, WagtailTestUtils):
fixtures = ['test_specific.json']
def setUp(self):
self.login()
def get(self, id):
return self.client.get('/admin/tests/eventpage/inspect/%d/' % id)
def test_simple(self):
response = self.get(4)
self.assertEqual(response.status_code, 200)
def test_title_present(self):
"""
The page title should appear twice. Once in the header, and once
more in the field listing
"""
response = self.get(4)
self.assertContains(response, 'Christmas', 2)
def test_location_present(self):
"""
The location should appear once, in the field listing
"""
response = self.get(4)
self.assertContains(response, 'The North Pole', 1)
def test_non_existent(self):
response = self.get(100)
self.assertEqual(response.status_code, 404)
class TestEditView(TestCase, WagtailTestUtils):
fixtures = ['test_specific.json']
def setUp(self):
self.login()
def get(self, obj_id):
return self.client.get('/admin/tests/eventpage/edit/%d/' % obj_id)
def test_simple(self):
response = self.get(4)
expected_path = '/admin/pages/4/edit/'
expected_next_path = '/admin/tests/eventpage/'
self.assertRedirects(response, '%s?next=%s' % (expected_path, expected_next_path))
def test_non_existent(self):
response = self.get(100)
self.assertEqual(response.status_code, 404)
class TestDeleteView(TestCase, WagtailTestUtils):
fixtures = ['test_specific.json']
def setUp(self):
self.login()
def get(self, obj_id):
return self.client.get('/admin/tests/eventpage/delete/%d/' % obj_id)
def test_simple(self):
response = self.get(4)
expected_path = '/admin/pages/4/delete/'
expected_next_path = '/admin/tests/eventpage/'
self.assertRedirects(response, '%s?next=%s' % (expected_path, expected_next_path))
class TestChooseParentView(TestCase, WagtailTestUtils):
fixtures = ['test_specific.json']
def setUp(self):
self.login()
def test_simple(self):
response = self.client.get('/admin/tests/eventpage/choose_parent/')
self.assertEqual(response.status_code, 200)
def test_no_parent_exists(self):
response = self.client.get('/admin/tests/businesschild/choose_parent/')
self.assertEqual(response.status_code, 403)
def test_post(self):
response = self.client.post('/admin/tests/eventpage/choose_parent/', {
'parent_page': 2,
})
expected_path = '/admin/pages/add/tests/eventpage/2/'
expected_next_path = '/admin/tests/eventpage/'
self.assertRedirects(response, '%s?next=%s' % (expected_path, expected_next_path))
class TestEditorAccess(TestCase):
fixtures = ['test_specific.json']
expected_status_code = 403
def login(self):
# Create a user
user = get_user_model().objects._create_user(username='test2', email='test2@email.com', password='password', is_staff=True, is_superuser=False)
user.groups.add(Group.objects.get(pk=2))
# Login
self.client.login(username='test2', password='password')
return user
def setUp(self):
self.login()
def test_delete_permitted(self):
response = self.client.get('/admin/tests/eventpage/delete/4/')
self.assertEqual(response.status_code, self.expected_status_code)
class TestModeratorAccess(TestCase):
fixtures = ['test_specific.json']
expected_status_code = 302
def login(self):
# Create a user
user = get_user_model().objects._create_user(username='test3', email='test3@email.com', password='password', is_staff=True, is_superuser=False)
user.groups.add(Group.objects.get(pk=1))
# Login
self.client.login(username='test2', password='password')
return user
def setUp(self):
self.login()
def test_delete_permitted(self):
response = self.client.get('/admin/tests/eventpage/delete/4/')
self.assertEqual(response.status_code, self.expected_status_code)

Wyświetl plik

@ -0,0 +1,357 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.test import TestCase
from wagtail.tests.modeladmintest.models import Book, Author
from wagtail.tests.utils import WagtailTestUtils
class TestIndexView(TestCase, WagtailTestUtils):
fixtures = ['modeladmintest_test.json']
def setUp(self):
self.login()
def get(self, **params):
return self.client.get('/admin/modeladmintest/book/', params)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
# There are four books in the test data
self.assertEqual(response.context['result_count'], 4)
# User has add permission
self.assertEqual(response.context['user_can_create'], True)
def test_filter(self):
# Filter by author 1 (JRR Tolkien)
response = self.get(author__id__exact=1)
self.assertEqual(response.status_code, 200)
# JRR Tolkien has two books in the test data
self.assertEqual(response.context['result_count'], 2)
for book in response.context['object_list']:
self.assertEqual(book.author_id, 1)
def test_search(self):
response = self.get(q='of')
self.assertEqual(response.status_code, 200)
# There are two books where the title contains 'of'
self.assertEqual(response.context['result_count'], 2)
def test_ordering(self):
response = self.get(o='0.1')
self.assertEqual(response.status_code, 200)
# There are four books in the test data
self.assertEqual(response.context['result_count'], 4)
def test_paging(self):
# should be corrected to just the first page, as there aren't enough
# objects to make up more than one page
response = self.get(p=9)
self.assertEqual(response.status_code, 200)
# There are four books in the test data
self.assertEqual(response.context['result_count'], 4)
# Should raise a ValueError that gets caught during initialisation
response = self.get(p='notaninteger')
self.assertEqual(response.status_code, 200)
# There are four books in the test data
self.assertEqual(response.context['result_count'], 4)
class TestCreateView(TestCase, WagtailTestUtils):
fixtures = ['modeladmintest_test.json']
def setUp(self):
self.login()
def get(self):
return self.client.get('/admin/modeladmintest/book/create/')
def post(self, post_data):
return self.client.post('/admin/modeladmintest/book/create/', post_data)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
def test_create(self):
response = self.post({
'title': "George's Marvellous Medicine",
'author': 2,
})
# Should redirect back to index
self.assertRedirects(response, '/admin/modeladmintest/book/')
# Check that the book was created
self.assertEqual(Book.objects.filter(title="George's Marvellous Medicine").count(), 1)
def test_post_invalid(self):
initial_book_count = Book.objects.count()
response = self.post({
'title': '',
'author': 2,
})
final_book_count = Book.objects.count()
self.assertEqual(response.status_code, 200)
# Check that the book was not created
self.assertEqual(initial_book_count, final_book_count)
# Check that a form error was raised
self.assertFormError(response, 'form', 'title', "This field is required.")
class TestInspectView(TestCase, WagtailTestUtils):
fixtures = ['modeladmintest_test.json']
def setUp(self):
self.login()
def get_for_author(self, author_id):
return self.client.get('/admin/modeladmintest/author/inspect/%d/' % author_id)
def get_for_book(self, book_id):
return self.client.get('/admin/modeladmintest/book/inspect/%d/' % book_id)
def author_test_simple(self):
response = self.get_for_author(1)
self.assertEqual(response.status_code, 200)
def author_test_name_present(self):
"""
The author name should appear twice. Once in the header, and once
more in the field listing
"""
response = self.get_for_author(1)
self.assertContains(response, 'J. R. R. Tolkien', 2)
def author_test_dob_not_present(self):
"""
The date of birth shouldn't appear, because the field wasn't included
in the `inspect_view_fields` list
"""
response = self.get_for_author(1)
self.assertNotContains(response, '1892', 2)
def book_test_simple(self):
response = self.get_for_book(1)
self.assertEqual(response.status_code, 200)
def book_test_title_present(self):
"""
The book title should appear once only, in the header, as 'title'
was added to the `inspect_view_fields_ignore` list
"""
response = self.get_for_book(1)
self.assertContains(response, 'The Lord of the Rings', 1)
def book_test_author_present(self):
"""
The author name should appear, because 'author' is not in
`inspect_view_fields_ignore` and should be returned by the
`get_inspect_view_fields` method.
"""
response = self.get_for_book(1)
self.assertContains(response, 'J. R. R. Tolkien', 1)
def test_non_existent(self):
response = self.get_for_book(100)
self.assertEqual(response.status_code, 404)
class TestEditView(TestCase, WagtailTestUtils):
fixtures = ['modeladmintest_test.json']
def setUp(self):
self.login()
def get(self, book_id):
return self.client.get('/admin/modeladmintest/book/edit/%d/' % book_id)
def post(self, book_id, post_data):
return self.client.post('/admin/modeladmintest/book/edit/%d/' % book_id, post_data)
def test_simple(self):
response = self.get(1)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'The Lord of the Rings')
def test_non_existent(self):
response = self.get(100)
self.assertEqual(response.status_code, 404)
def test_edit(self):
response = self.post(1, {
'title': 'The Lady of the Rings',
'author': 1,
})
# Should redirect back to index
self.assertRedirects(response, '/admin/modeladmintest/book/')
# Check that the book was updated
self.assertEqual(Book.objects.get(id=1).title, 'The Lady of the Rings')
def test_post_invalid(self):
response = self.post(1, {
'title': '',
'author': 1,
})
self.assertEqual(response.status_code, 200)
# Check that the title was not updated
self.assertEqual(Book.objects.get(id=1).title, 'The Lord of the Rings')
# Check that a form error was raised
self.assertFormError(response, 'form', 'title', "This field is required.")
class TestPageSpecificViews(TestCase, WagtailTestUtils):
fixtures = ['modeladmintest_test.json']
expected_status_code = 404
def setUp(self):
self.login()
def test_choose_parent(self):
response = self.client.get('/admin/modeladmintest/book/choose_parent/')
self.assertEqual(response.status_code, self.expected_status_code)
class TestConfirmDeleteView(TestCase, WagtailTestUtils):
fixtures = ['modeladmintest_test.json']
def setUp(self):
self.login()
def get(self, book_id):
return self.client.get('/admin/modeladmintest/book/delete/%d/' % book_id)
def post(self, book_id):
return self.client.post('/admin/modeladmintest/book/delete/%d/' % book_id)
def test_simple(self):
response = self.get(1)
self.assertEqual(response.status_code, 200)
def test_non_existent(self):
response = self.get(100)
self.assertEqual(response.status_code, 404)
def test_post(self):
response = self.post(1)
# User redirected to index
self.assertRedirects(response, '/admin/modeladmintest/book/')
# Book deleted
self.assertFalse(Book.objects.filter(id=1).exists())
class TestDeleteViewWithProtectedRelation(TestCase, WagtailTestUtils):
fixtures = ['modeladmintest_test.json']
def setUp(self):
self.login()
def get(self, author_id):
return self.client.get('/admin/modeladmintest/author/delete/%d/' % author_id)
def post(self, author_id):
return self.client.post('/admin/modeladmintest/author/delete/%d/' % author_id)
def test_get_with_dependent_object(self):
response = self.get(1)
self.assertEqual(response.status_code, 200)
def test_get_without_dependent_object(self):
response = self.get(4)
self.assertEqual(response.status_code, 200)
def test_post_with_dependent_object(self):
response = self.post(1)
self.assertEqual(response.status_code, 200)
self.assertContains(
response,
"'J. R. R. Tolkien' is currently referenced by other objects"
)
# Author not deleted
self.assertTrue(Author.objects.filter(id=1).exists())
def test_post_without_dependent_object(self):
response = self.post(4)
# User redirected to index
self.assertRedirects(response, '/admin/modeladmintest/author/')
# Author deleted
self.assertFalse(Author.objects.filter(id=4).exists())
class TestEditorAccess(TestCase):
fixtures = ['modeladmintest_test.json']
expected_status_code = 403
def login(self):
# Create a user
user = get_user_model().objects._create_user(username='test2', email='test2@email.com', password='password', is_staff=True, is_superuser=False)
user.groups.add(Group.objects.get(pk=2))
# Login
self.client.login(username='test2', password='password')
return user
def setUp(self):
self.login()
def test_index_permitted(self):
response = self.client.get('/admin/modeladmintest/book/')
self.assertEqual(response.status_code, self.expected_status_code)
def test_inpspect_permitted(self):
response = self.client.get('/admin/modeladmintest/book/inspect/2/')
self.assertEqual(response.status_code, self.expected_status_code)
def test_create_permitted(self):
response = self.client.get('/admin/modeladmintest/book/create/')
self.assertEqual(response.status_code, self.expected_status_code)
def test_edit_permitted(self):
response = self.client.get('/admin/modeladmintest/book/edit/2/')
self.assertEqual(response.status_code, self.expected_status_code)
def test_delete_get_permitted(self):
response = self.client.get('/admin/modeladmintest/book/delete/2/')
self.assertEqual(response.status_code, self.expected_status_code)
def test_delete_post_permitted(self):
response = self.client.post('/admin/modeladmintest/book/delete/2/')
self.assertEqual(response.status_code, self.expected_status_code)

Wyświetl plik

@ -0,0 +1,932 @@
from __future__ import absolute_import, unicode_literals
import operator
import sys
from collections import OrderedDict
from functools import reduce
from django import forms
from django.contrib.admin import FieldListFilter, widgets
from django.contrib.admin.exceptions import DisallowedModelAdminLookup
from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.utils import (
get_fields_from_path, lookup_needs_distinct, prepare_lookup_value, quote, unquote)
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ImproperlyConfigured, PermissionDenied, SuspiciousOperation
from django.core.paginator import InvalidPage, Paginator
from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields import FieldDoesNotExist
from django.db.models.fields.related import ForeignObjectRel
from django.db.models.sql.constants import QUERY_TERMS
from django.shortcuts import get_object_or_404, redirect, render
from django.template.defaultfilters import filesizeformat
from django.utils import six
from django.utils.decorators import method_decorator
from django.utils.encoding import force_text
from django.utils.functional import cached_property
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
from django.views.generic.edit import FormView
from wagtail.wagtailadmin import messages
from wagtail.wagtailadmin.edit_handlers import (
ObjectList, extract_panel_definitions_from_model_class)
from wagtail.wagtaildocs.models import get_document_model
from wagtail.wagtailimages.models import Filter, get_image_model
from .forms import ParentChooserForm
class WMABaseView(TemplateView):
"""
Groups together common functionality for all app views.
"""
model_admin = None
meta_title = ''
page_title = ''
page_subtitle = ''
def __init__(self, model_admin):
self.model_admin = model_admin
self.model = model_admin.model
self.opts = self.model._meta
self.app_label = force_text(self.opts.app_label)
self.model_name = force_text(self.opts.model_name)
self.verbose_name = force_text(self.opts.verbose_name)
self.verbose_name_plural = force_text(self.opts.verbose_name_plural)
self.pk_attname = self.opts.pk.attname
self.is_pagemodel = model_admin.is_pagemodel
self.permission_helper = model_admin.permission_helper
self.url_helper = model_admin.url_helper
def check_action_permitted(self, user):
return True
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
if not self.check_action_permitted(request.user):
raise PermissionDenied
button_helper_class = self.model_admin.get_button_helper_class()
self.button_helper = button_helper_class(self, request)
return super(WMABaseView, self).dispatch(request, *args, **kwargs)
@cached_property
def menu_icon(self):
return self.model_admin.get_menu_icon()
@cached_property
def header_icon(self):
return self.menu_icon
def get_page_title(self):
return self.page_title or capfirst(self.opts.verbose_name_plural)
def get_meta_title(self):
return self.meta_title or self.get_page_title()
@cached_property
def index_url(self):
return self.url_helper.index_url
@cached_property
def create_url(self):
return self.url_helper.create_url
def get_base_queryset(self, request=None):
return self.model_admin.get_queryset(request or self.request)
class ModelFormView(WMABaseView, FormView):
def get_edit_handler_class(self):
if hasattr(self.model, 'edit_handler'):
edit_handler = self.model.edit_handler
else:
panels = extract_panel_definitions_from_model_class(self.model)
edit_handler = ObjectList(panels)
return edit_handler.bind_to_model(self.model)
def get_form_class(self):
return self.get_edit_handler_class().get_form_class(self.model)
def get_success_url(self):
return self.index_url
def get_instance(self):
return getattr(self, 'instance', None) or self.model()
def get_form_kwargs(self):
kwargs = FormView.get_form_kwargs(self)
kwargs.update({'instance': self.get_instance()})
return kwargs
@property
def media(self):
return forms.Media(
css={'all': self.model_admin.get_form_view_extra_css()},
js=self.model_admin.get_form_view_extra_js()
)
def get_context_data(self, **kwargs):
context = super(ModelFormView, self).get_context_data(**kwargs)
instance = self.get_instance()
edit_handler_class = self.get_edit_handler_class()
form = self.get_form()
context.update({
'view': self,
'model_admin': self.model_admin,
'is_multipart': form.is_multipart(),
'edit_handler': edit_handler_class(instance=instance, form=form),
'form': form,
})
return context
def get_success_message(self, instance):
return _("{model_name} '{instance}' created.").format(
model_name=capfirst(self.opts.verbose_name), instance=instance)
def get_success_message_buttons(self, instance):
button_url = self.url_helper.get_action_url('edit', quote(instance.pk))
return [
messages.button(button_url, _('Edit'))
]
def get_error_message(self):
model_name = self.verbose_name
return _("The %s could not be created due to errors.") % model_name
def form_valid(self, form):
instance = form.save()
messages.success(
self.request, self.get_success_message(instance),
buttons=self.get_success_message_buttons(instance)
)
return redirect(self.get_success_url())
def form_invalid(self, form):
messages.error(self.request, self.get_error_message())
return self.render_to_response(self.get_context_data())
class InstanceSpecificView(WMABaseView):
instance_pk = None
pk_quoted = None
instance = None
def __init__(self, model_admin, instance_pk):
super(InstanceSpecificView, self).__init__(model_admin)
self.instance_pk = unquote(instance_pk)
self.pk_quoted = quote(self.instance_pk)
filter_kwargs = {}
filter_kwargs[self.pk_attname] = self.instance_pk
object_qs = model_admin.model._default_manager.get_queryset().filter(
**filter_kwargs)
self.instance = get_object_or_404(object_qs)
def get_page_subtitle(self):
return self.instance
@cached_property
def edit_url(self):
return self.url_helper.get_action_url('edit', self.pk_quoted)
@cached_property
def delete_url(self):
return self.url_helper.get_action_url('delete', self.pk_quoted)
class IndexView(WMABaseView):
# IndexView settings
ORDER_VAR = 'o'
ORDER_TYPE_VAR = 'ot'
PAGE_VAR = 'p'
SEARCH_VAR = 'q'
ERROR_FLAG = 'e'
IGNORED_PARAMS = (ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR)
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
# Only continue if logged in user has list permission
if not self.permission_helper.user_can_list(request.user):
raise PermissionDenied
self.list_display = self.model_admin.get_list_display(request)
self.list_filter = self.model_admin.get_list_filter(request)
self.search_fields = self.model_admin.get_search_fields(request)
self.items_per_page = self.model_admin.list_per_page
self.select_related = self.model_admin.list_select_related
# Get search parameters from the query string.
try:
self.page_num = int(request.GET.get(self.PAGE_VAR, 0))
except ValueError:
self.page_num = 0
self.params = dict(request.GET.items())
if self.PAGE_VAR in self.params:
del self.params[self.PAGE_VAR]
if self.ERROR_FLAG in self.params:
del self.params[self.ERROR_FLAG]
self.query = request.GET.get(self.SEARCH_VAR, '')
self.queryset = self.get_queryset(request)
return super(IndexView, self).dispatch(request, *args, **kwargs)
@property
def media(self):
return forms.Media(
css={'all': self.model_admin.get_index_view_extra_css()},
js=self.model_admin.get_index_view_extra_js()
)
def get_buttons_for_obj(self, obj):
return self.button_helper.get_buttons_for_obj(
obj, classnames_add=['button-small', 'button-secondary'])
def get_search_results(self, request, queryset, search_term):
"""
Returns a tuple containing a queryset to implement the search,
and a boolean indicating if the results may contain duplicates.
"""
use_distinct = False
if self.search_fields and search_term:
orm_lookups = ['%s__icontains' % str(search_field)
for search_field in self.search_fields]
for bit in search_term.split():
or_queries = [models.Q(**{orm_lookup: bit})
for orm_lookup in orm_lookups]
queryset = queryset.filter(reduce(operator.or_, or_queries))
if not use_distinct:
for search_spec in orm_lookups:
if lookup_needs_distinct(self.opts, search_spec):
use_distinct = True
break
return queryset, use_distinct
def lookup_allowed(self, lookup, value):
# Check FKey lookups that are allowed, so that popups produced by
# ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
# are allowed to work.
for l in self.model._meta.related_fkey_lookups:
for k, v in widgets.url_params_from_lookup_dict(l).items():
if k == lookup and v == value:
return True
parts = lookup.split(LOOKUP_SEP)
# Last term in lookup is a query term (__exact, __startswith etc)
# This term can be ignored.
if len(parts) > 1 and parts[-1] in QUERY_TERMS:
parts.pop()
# Special case -- foo__id__exact and foo__id queries are implied
# if foo has been specifically included in the lookup list; so
# drop __id if it is the last part. However, first we need to find
# the pk attribute name.
rel_name = None
for part in parts[:-1]:
try:
field = self.model._meta.get_field(part)
except FieldDoesNotExist:
# Lookups on non-existent fields are ok, since they're ignored
# later.
return True
if hasattr(field, 'rel'):
if field.rel is None:
# This property or relation doesn't exist, but it's allowed
# since it's ignored in ChangeList.get_filters().
return True
model = field.rel.to
rel_name = field.rel.get_related_field().name
elif isinstance(field, ForeignObjectRel):
model = field.model
rel_name = model._meta.pk.name
else:
rel_name = None
if rel_name and len(parts) > 1 and parts[-1] == rel_name:
parts.pop()
if len(parts) == 1:
return True
clean_lookup = LOOKUP_SEP.join(parts)
return clean_lookup in self.list_filter
def get_filters_params(self, params=None):
"""
Returns all params except IGNORED_PARAMS
"""
if not params:
params = self.params
lookup_params = params.copy() # a dictionary of the query string
# Remove all the parameters that are globally and systematically
# ignored.
for ignored in self.IGNORED_PARAMS:
if ignored in lookup_params:
del lookup_params[ignored]
return lookup_params
def get_filters(self, request):
lookup_params = self.get_filters_params()
use_distinct = False
for key, value in lookup_params.items():
if not self.lookup_allowed(key, value):
raise DisallowedModelAdminLookup(
"Filtering by %s not allowed" % key)
filter_specs = []
if self.list_filter:
for list_filter in self.list_filter:
if callable(list_filter):
# This is simply a custom list filter class.
spec = list_filter(
request,
lookup_params,
self.model,
self.model_admin)
else:
field_path = None
if isinstance(list_filter, (tuple, list)):
# This is a custom FieldListFilter class for a given
# field.
field, field_list_filter_class = list_filter
else:
# This is simply a field name, so use the default
# FieldListFilter class that has been registered for
# the type of the given field.
field = list_filter
field_list_filter_class = FieldListFilter.create
if not isinstance(field, models.Field):
field_path = field
field = get_fields_from_path(self.model,
field_path)[-1]
spec = field_list_filter_class(
field,
request,
lookup_params,
self.model,
self.model_admin,
field_path=field_path)
# Check if we need to use distinct()
use_distinct = (
use_distinct or lookup_needs_distinct(self.opts,
field_path))
if spec and spec.has_output():
filter_specs.append(spec)
# At this point, all the parameters used by the various ListFilters
# have been removed from lookup_params, which now only contains other
# parameters passed via the query string. We now loop through the
# remaining parameters both to ensure that all the parameters are valid
# fields and to determine if at least one of them needs distinct(). If
# the lookup parameters aren't real fields, then bail out.
try:
for key, value in lookup_params.items():
lookup_params[key] = prepare_lookup_value(key, value)
use_distinct = (
use_distinct or lookup_needs_distinct(self.opts, key))
return (
filter_specs, bool(filter_specs), lookup_params, use_distinct
)
except FieldDoesNotExist as e:
six.reraise(
IncorrectLookupParameters,
IncorrectLookupParameters(e),
sys.exc_info()[2])
def get_query_string(self, new_params=None, remove=None):
if new_params is None:
new_params = {}
if remove is None:
remove = []
p = self.params.copy()
for r in remove:
for k in list(p):
if k.startswith(r):
del p[k]
for k, v in new_params.items():
if v is None:
if k in p:
del p[k]
else:
p[k] = v
return '?%s' % urlencode(sorted(p.items()))
def _get_default_ordering(self):
ordering = []
if self.model_admin.ordering:
ordering = self.model_admin.ordering
elif self.opts.ordering:
ordering = self.opts.ordering
return ordering
def get_default_ordering(self, request):
if self.model_admin.get_ordering(request):
return self.model_admin.get_ordering(request)
if self.opts.ordering:
return self.opts.ordering
return ()
def get_ordering_field(self, field_name):
"""
Returns the proper model field name corresponding to the given
field_name to use for ordering. field_name may either be the name of a
proper model field or the name of a method (on the admin or model) or a
callable with the 'admin_order_field' attribute. Returns None if no
proper model field name can be matched.
"""
try:
field = self.opts.get_field(field_name)
return field.name
except FieldDoesNotExist:
# See whether field_name is a name of a non-field
# that allows sorting.
if callable(field_name):
attr = field_name
elif hasattr(self.model_admin, field_name):
attr = getattr(self.model_admin, field_name)
else:
attr = getattr(self.model, field_name)
return getattr(attr, 'admin_order_field', None)
def get_ordering(self, request, queryset):
"""
Returns the list of ordering fields for the change list.
First we check the get_ordering() method in model admin, then we check
the object's default ordering. Then, any manually-specified ordering
from the query string overrides anything. Finally, a deterministic
order is guaranteed by ensuring the primary key is used as the last
ordering field.
"""
params = self.params
ordering = list(self.get_default_ordering(request))
if self.ORDER_VAR in params:
# Clear ordering and used params
ordering = []
order_params = params[self.ORDER_VAR].split('.')
for p in order_params:
try:
none, pfx, idx = p.rpartition('-')
field_name = self.list_display[int(idx)]
order_field = self.get_ordering_field(field_name)
if not order_field:
continue # No 'admin_order_field', skip it
# reverse order if order_field has already "-" as prefix
if order_field.startswith('-') and pfx == "-":
ordering.append(order_field[1:])
else:
ordering.append(pfx + order_field)
except (IndexError, ValueError):
continue # Invalid ordering specified, skip it.
# Add the given query's ordering fields, if any.
ordering.extend(queryset.query.order_by)
# Ensure that the primary key is systematically present in the list of
# ordering fields so we can guarantee a deterministic order across all
# database backends.
pk_name = self.opts.pk.name
if not (set(ordering) & {'pk', '-pk', pk_name, '-' + pk_name}):
# The two sets do not intersect, meaning the pk isn't present. So
# we add it.
ordering.append('-pk')
return ordering
def get_ordering_field_columns(self):
"""
Returns an OrderedDict of ordering field column numbers and asc/desc
"""
# We must cope with more than one column having the same underlying
# sort field, so we base things on column numbers.
ordering = self._get_default_ordering()
ordering_fields = OrderedDict()
if self.ORDER_VAR not in self.params:
# for ordering specified on model_admin or model Meta, we don't
# know the right column numbers absolutely, because there might be
# morr than one column associated with that ordering, so we guess.
for field in ordering:
if field.startswith('-'):
field = field[1:]
order_type = 'desc'
else:
order_type = 'asc'
for index, attr in enumerate(self.list_display):
if self.get_ordering_field(attr) == field:
ordering_fields[index] = order_type
break
else:
for p in self.params[self.ORDER_VAR].split('.'):
none, pfx, idx = p.rpartition('-')
try:
idx = int(idx)
except ValueError:
continue # skip it
ordering_fields[idx] = 'desc' if pfx == '-' else 'asc'
return ordering_fields
def get_queryset(self, request=None):
request = request or self.request
# First, we collect all the declared list filters.
(self.filter_specs, self.has_filters, remaining_lookup_params,
filters_use_distinct) = self.get_filters(request)
# Then, we let every list filter modify the queryset to its liking.
qs = self.get_base_queryset(request)
for filter_spec in self.filter_specs:
new_qs = filter_spec.queryset(request, qs)
if new_qs is not None:
qs = new_qs
try:
# Finally, we apply the remaining lookup parameters from the query
# string (i.e. those that haven't already been processed by the
# filters).
qs = qs.filter(**remaining_lookup_params)
except (SuspiciousOperation, ImproperlyConfigured):
# Allow certain types of errors to be re-raised as-is so that the
# caller can treat them in a special way.
raise
except Exception as e:
# Every other error is caught with a naked except, because we don't
# have any other way of validating lookup parameters. They might be
# invalid if the keyword arguments are incorrect, or if the values
# are not in the correct type, so we might get FieldError,
# ValueError, ValidationError, or ?.
raise IncorrectLookupParameters(e)
if not qs.query.select_related:
qs = self.apply_select_related(qs)
# Set ordering.
ordering = self.get_ordering(request, qs)
qs = qs.order_by(*ordering)
# Apply search results
qs, search_use_distinct = self.get_search_results(
request, qs, self.query)
# Remove duplicates from results, if necessary
if filters_use_distinct | search_use_distinct:
return qs.distinct()
else:
return qs
def apply_select_related(self, qs):
if self.select_related is True:
return qs.select_related()
if self.select_related is False:
if self.has_related_field_in_list_display():
return qs.select_related()
if self.select_related:
return qs.select_related(*self.select_related)
return qs
def has_related_field_in_list_display(self):
for field_name in self.list_display:
try:
field = self.opts.get_field(field_name)
except FieldDoesNotExist:
pass
else:
if isinstance(field, models.ManyToOneRel):
return True
return False
def get_context_data(self, *args, **kwargs):
user = self.request.user
all_count = self.get_base_queryset().count()
queryset = self.get_queryset()
result_count = queryset.count()
paginator = Paginator(queryset, self.items_per_page)
try:
page_obj = paginator.page(self.page_num + 1)
except InvalidPage:
page_obj = paginator.page(1)
context = {
'view': self,
'all_count': all_count,
'result_count': result_count,
'paginator': paginator,
'page_obj': page_obj,
'object_list': page_obj.object_list,
'user_can_create': self.permission_helper.user_can_create(user)
}
if self.is_pagemodel:
models = self.model.allowed_parent_page_models()
allowed_parent_types = [m._meta.verbose_name for m in models]
valid_parents = self.permission_helper.get_valid_parent_pages(user)
valid_parent_count = valid_parents.count()
context.update({
'no_valid_parents': not valid_parent_count,
'required_parent_types': allowed_parent_types,
})
return context
def get_template_names(self):
return self.model_admin.get_index_template()
class CreateView(ModelFormView):
page_title = _('New')
def check_action_permitted(self, user):
return self.permission_helper.user_can_create(user)
def dispatch(self, request, *args, **kwargs):
if self.is_pagemodel:
user = request.user
parents = self.permission_helper.get_valid_parent_pages(user)
parent_count = parents.count()
# There's only one available parent for this page type for this
# user, so we send them along with that as the chosen parent page
if parent_count == 1:
parent = parents.get()
parent_pk = quote(parent.pk)
return redirect(self.url_helper.get_action_url(
'add', self.app_label, self.model_name, parent_pk))
# The page can be added in multiple places, so redirect to the
# choose_parent view so that the parent can be specified
return redirect(self.url_helper.get_action_url('choose_parent'))
return super(CreateView, self).dispatch(request, *args, **kwargs)
def get_meta_title(self):
return _('Create new %s') % self.verbose_name
def get_page_subtitle(self):
return capfirst(self.verbose_name)
def get_template_names(self):
return self.model_admin.get_create_template()
class EditView(ModelFormView, InstanceSpecificView):
page_title = _('Editing')
def check_action_permitted(self, user):
return self.permission_helper.user_can_edit_obj(user, self.instance)
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
if self.is_pagemodel:
return redirect(
self.url_helper.get_action_url('edit', self.pk_quoted)
)
return super(EditView, self).dispatch(request, *args, **kwargs)
def get_meta_title(self):
return _('Editing %s') % self.verbose_name
def get_success_message(self, instance):
return _("{model_name} '{instance}' updated.").format(
model_name=capfirst(self.verbose_name), instance=instance)
def get_context_data(self, **kwargs):
kwargs['user_can_delete'] = self.permission_helper.user_can_delete_obj(
self.request.user, self.instance)
return super(EditView, self).get_context_data(**kwargs)
def get_error_message(self):
name = self.verbose_name
return _("The %s could not be saved due to errors.") % name
def get_template_names(self):
return self.model_admin.get_edit_template()
class ChooseParentView(WMABaseView):
def dispatch(self, request, *args, **kwargs):
if not self.permission_helper.user_can_create(request.user):
raise PermissionDenied
return super(ChooseParentView, self).dispatch(request, *args, **kwargs)
def get_page_title(self):
return _('Add %s') % self.verbose_name
def get_form(self, request):
parents = self.permission_helper.get_valid_parent_pages(request.user)
return ParentChooserForm(parents, request.POST or None)
def get(self, request, *args, **kwargs):
form = self.get_form(request)
context = {'view': self, 'form': form}
return render(request, self.get_template(), context)
def post(self, request, *args, **kargs):
form = self.get_form(request)
if form.is_valid():
parent_pk = quote(form.cleaned_data['parent_page'].pk)
return redirect(self.url_helper.get_action_url(
'add', self.app_label, self.model_name, parent_pk))
context = {'view': self, 'form': form}
return render(request, self.get_template(), context)
def get_template(self):
return self.model_admin.get_choose_parent_template()
class DeleteView(InstanceSpecificView):
page_title = _('Delete')
def check_action_permitted(self, user):
return self.permission_helper.user_can_delete_obj(user, self.instance)
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
if not self.check_action_permitted(request.user):
raise PermissionDenied
if self.is_pagemodel:
return redirect(
self.url_helper.get_action_url('delete', self.pk_quoted)
)
return super(DeleteView, self).dispatch(request, *args, **kwargs)
def get_meta_title(self):
return _('Confirm deletion of %s') % self.verbose_name
def confirmation_message(self):
return _(
"Are you sure you want to delete this %s? If other things in your "
"site are related to it, they may also be affected."
) % self.verbose_name
def get(self, request, *args, **kwargs):
context = {'view': self, 'instance': self.instance}
return self.render_to_response(context)
def delete_instance(self):
self.instance.delete()
def post(self, request, *args, **kwargs):
try:
self.delete_instance()
messages.success(
request,
_("{model} '{instance}' deleted.").format(
model=self.verbose_name, instance=self.instance))
return redirect(self.index_url)
except models.ProtectedError:
linked_objects = []
for rel in self.model._meta.get_all_related_objects():
if rel.on_delete == models.PROTECT:
qs = getattr(self.instance, rel.get_accessor_name())
for obj in qs.all():
linked_objects.append(obj)
context = {
'view': self,
'instance': self.instance,
'protected_error': True,
'linked_objects': linked_objects,
}
return self.render_to_response(context)
def get_template_names(self):
return self.model_admin.get_delete_template()
class InspectView(InstanceSpecificView):
page_title = _('Inspecting')
def check_action_permitted(self, user):
return self.permission_helper.user_can_inspect_obj(user, self.instance)
@property
def media(self):
return forms.Media(
css={'all': self.model_admin.get_inspect_view_extra_css()},
js=self.model_admin.get_inspect_view_extra_js()
)
def get_meta_title(self):
return _('Inspecting %s') % self.verbose_name
def get_field_label(self, field_name, field=None):
""" Return a label to display for a field """
label = None
if field is not None:
label = getattr(field, 'verbose_name', None)
if label is None:
label = getattr(field, 'name', None)
if label is None:
label = field_name
return label
def get_field_display_value(self, field_name, field=None):
""" Return a display value for a field """
# First we check for a 'get_fieldname_display' property/method on
# the model, and return the value of that, if present.
val_funct = getattr(self.instance, 'get_%s_display' % field_name, None)
if val_funct is not None:
if callable(val_funct):
return val_funct()
return val_funct
# If we have a real field, we can utilise that to try to display
# something more useful
if field is not None:
try:
field_type = field.get_internal_type()
if (
field_type == 'ForeignKey' and
field.related_model == get_image_model()
):
# The field is an image
return self.get_image_field_display(field_name, field)
if (
field_type == 'ForeignKey' and
field.related_model == get_document_model()
):
# The field is a document
return self.get_document_field_display(field_name, field)
except AttributeError:
pass
# Resort to getting the value of 'field_name' from the instance
return getattr(self.instance, field_name,
self.model_admin.get_empty_value_display(field_name))
def get_image_field_display(self, field_name, field):
""" Render an image """
image = getattr(self.instance, field_name)
if image:
fltr, _ = Filter.objects.get_or_create(spec='max-400x400')
rendition = image.get_rendition(fltr)
return rendition.img_tag
return self.model_admin.get_empty_value_display(field_name)
def get_document_field_display(self, field_name, field):
""" Render a link to a document """
document = getattr(self.instance, field_name)
if document:
return mark_safe(
'<a href="%s">%s <span class="meta">(%s, %s)</span></a>' % (
document.url,
document.title,
document.file_extension.upper(),
filesizeformat(document.file.size),
)
)
return self.model_admin.get_empty_value_display(field_name)
def get_dict_for_field(self, field_name):
"""
Return a dictionary containing `label` and `value` values to display
for a field.
"""
try:
field = self.model._meta.get_field(field_name)
except FieldDoesNotExist:
field = None
return {
'label': self.get_field_label(field_name, field),
'value': self.get_field_display_value(field_name, field),
}
def get_fields_dict(self):
"""
Return a list of `label`/`value` dictionaries to represent the
fiels named by the model_admin class's `get_inspect_view_fields` method
"""
fields = []
for field_name in self.model_admin.get_inspect_view_fields():
fields.append(self.get_dict_for_field(field_name))
return fields
def get_context_data(self, **kwargs):
context = super(InspectView, self).get_context_data(**kwargs)
buttons = self.button_helper.get_buttons_for_obj(
self.instance, exclude=['inspect'])
context.update({
'view': self,
'fields': self.get_fields_dict(),
'buttons': buttons,
'instance': self.instance,
})
return context
def get_template_names(self):
return self.model_admin.get_inspect_template()

Wyświetl plik

@ -0,0 +1,7 @@
from django.apps import AppConfig
class WagtailTestsAppConfig(AppConfig):
name = 'wagtail.tests.modeladmintest'
label = 'test_modeladmintest'
verbose_name = "Test Wagtail Model Admin"

Wyświetl plik

@ -0,0 +1,66 @@
[
{
"pk": 1,
"model": "modeladmintest.author",
"fields": {
"name": "J. R. R. Tolkien",
"date_of_birth": "1892-01-03"
}
},
{
"pk": 2,
"model": "modeladmintest.author",
"fields": {
"name": "Roald Dahl",
"date_of_birth": "1916-09-13"
}
},
{
"pk": 3,
"model": "modeladmintest.author",
"fields": {
"name": "Roald Dahl",
"date_of_birth": "1898-11-29"
}
},
{
"pk": 4,
"model": "modeladmintest.author",
"fields": {
"name": "J. R. Hartley",
"date_of_birth": "1898-11-29"
}
},
{
"pk": 1,
"model": "modeladmintest.book",
"fields": {
"title": "The Lord of the Rings",
"author_id": 1
}
},
{
"pk": 2,
"model": "modeladmintest.book",
"fields": {
"title": "The Hobbit",
"author_id": 1
}
},
{
"pk": 3,
"model": "modeladmintest.book",
"fields": {
"title": "Charlie and the Chocolate Factory",
"author_id": 2
}
},
{
"pk": 4,
"model": "modeladmintest.book",
"fields": {
"title": "The Chronicles of Narnia",
"author_id": 3
}
}
]

Wyświetl plik

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-22 11:40
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import wagtail.wagtailsearch.index
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Author',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('date_of_birth', models.DateField()),
],
),
migrations.CreateModel(
name='Book',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='modeladmintest.Author')),
],
bases=(models.Model, wagtail.wagtailsearch.index.Indexed),
),
]

Wyświetl plik

@ -0,0 +1,22 @@
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from wagtail.wagtailsearch import index
@python_2_unicode_compatible
class Author(models.Model):
name = models.CharField(max_length=255)
date_of_birth = models.DateField()
def __str__(self):
return self.name
@python_2_unicode_compatible
class Book(models.Model, index.Indexed):
author = models.ForeignKey(Author, on_delete=models.PROTECT)
title = models.CharField(max_length=255)
def __str__(self):
return self.title

Wyświetl plik

@ -0,0 +1,57 @@
from wagtail.contrib.modeladmin.options import (
ModelAdmin, ModelAdminGroup, modeladmin_register)
from .models import Author, Book
from wagtail.tests.testapp.models import (
EventPage, SingleEventPage, BusinessChild
)
class AuthorModelAdmin(ModelAdmin):
model = Author
menu_order = 200
list_display = ('name', 'date_of_birth')
list_filter = ('date_of_birth', )
search_fields = ('name', )
inspect_view_enabled = True
inspect_view_fields = ('name', )
class BookModelAdmin(ModelAdmin):
model = Book
menu_order = 300
list_display = ('title', 'author')
list_filter = ('author', )
search_fields = ('title', )
inspect_view_enabled = True
inspect_view_fields_exclude = ('title', )
class EventPageAdmin(ModelAdmin):
model = EventPage
list_display = ('title', 'date_from', 'audience')
list_filter = ('audience', )
search_fields = ('title', )
inspect_view_enabled = True
inspect_view_fields_exclude = ('feed_image', )
class SingleEventPageAdmin(EventPageAdmin):
model = SingleEventPage
class EventsAdminGroup(ModelAdminGroup):
menu_label = "Events"
items = (EventPageAdmin, SingleEventPageAdmin)
menu_order = 500
class BusinessChildAdmin(ModelAdmin):
model = BusinessChild
# having "Business Child" permanently in the menu confuses tests for the 'add page' view
menu_label = "BusinessSprog"
modeladmin_register(AuthorModelAdmin)
modeladmin_register(BookModelAdmin)
modeladmin_register(EventsAdminGroup)
modeladmin_register(BusinessChildAdmin)

Wyświetl plik

@ -77,7 +77,6 @@ MIDDLEWARE_CLASSES = (
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'wagtail.wagtailcore.middleware.SiteMiddleware',
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
)
@ -93,6 +92,7 @@ INSTALLED_APPS = (
'wagtail.tests.snippets',
'wagtail.tests.routablepage',
'wagtail.tests.search',
'wagtail.tests.modeladmintest',
'wagtail.contrib.wagtailstyleguide',
'wagtail.contrib.wagtailsitemaps',
'wagtail.contrib.wagtailroutablepage',
@ -100,6 +100,7 @@ INSTALLED_APPS = (
'wagtail.contrib.wagtailapi',
'wagtail.contrib.wagtailsearchpromotions',
'wagtail.contrib.settings',
'wagtail.contrib.modeladmin',
'wagtail.wagtailforms',
'wagtail.wagtailsearch',
'wagtail.wagtailembeds',

Wyświetl plik

@ -23,6 +23,7 @@
{% endif %}
<form action="{% url 'wagtailadmin_pages:delete' page.id %}" method="POST">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
<input type="submit" value="{% trans 'Delete it' %}" class="serious {% if page.live %}button-secondary{% endif %}"> {% if page_perms.can_unpublish %}<a href="{% url 'wagtailadmin_pages:unpublish' page.id %}" class="button">{% trans 'Unpublish it' %}</a>{% endif %}
</form>
</div>

Wyświetl plik

@ -9,6 +9,7 @@
<p>{% trans "Are you sure you want to unpublish this page?" %}</p>
<form action="{% url 'wagtailadmin_pages:unpublish' page.id %}" method="POST">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
<input type="submit" value="{% trans 'Yes, unpublish it' %}">
</form>
</div>

Wyświetl plik

@ -8,6 +8,7 @@
<div class="nice-padding">
<form action="{% url 'wagtailadmin_pages:copy' page.id %}" method="POST">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
<ul class="fields">
{% include "wagtailadmin/shared/field_as_li.html" with field=form.new_title %}

Wyświetl plik

@ -19,6 +19,7 @@
<form id="page-edit-form" action="{% url 'wagtailadmin_pages:add' content_type.app_label content_type.model parent_page.id %}" method="POST"{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
{{ edit_handler.render_form_content }}
{% page_permissions parent_page as parent_page_perms %}

Wyświetl plik

@ -26,6 +26,7 @@
<form id="page-edit-form" action="{% url 'wagtailadmin_pages:edit' page.id %}" method="POST"{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
{{ edit_handler.render_form_content }}
{% if is_revision %}

Wyświetl plik

@ -9,6 +9,8 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.http import is_safe_url
from django.utils.translation import ugettext as _
from django.utils.http import is_safe_url, urlquote
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_GET, require_POST
@ -22,6 +24,13 @@ from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page, PageRevision, get_navigation_menu_items
def get_valid_next_url_from_request(request):
next_url = request.POST.get('next') or request.GET.get('next')
if not next_url or not is_safe_url(url=next_url, host=request.get_host()):
return ''
return next_url
def explorer_nav(request):
return render(request, 'wagtailadmin/shared/explorer_nav.html', {
'nodes': get_navigation_menu_items(),
@ -172,6 +181,8 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
edit_handler_class = page_class.get_edit_handler()
form_class = edit_handler_class.get_form_class(page_class)
next_url = get_valid_next_url_from_request(request)
if request.method == 'POST':
form = form_class(request.POST, request.FILES, instance=page,
parent_page=parent_page)
@ -229,11 +240,19 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
return result
if is_publishing or is_submitting:
# we're done here - redirect back to the explorer
# we're done here
if next_url:
# redirect back to 'next' url if present
return redirect(next_url)
# redirect back to the explorer
return redirect('wagtailadmin_explore', page.get_parent().id)
else:
# Just saving - remain on edit page for further edits
return redirect('wagtailadmin_pages:edit', page.id)
target_url = reverse('wagtailadmin_pages:edit', args=[page.id])
if next_url:
# Ensure the 'next' url is passed through again if present
target_url += '?next=%s' % urlquote(next_url)
return redirect(target_url)
else:
messages.error(request, _("The page could not be created due to validation errors"))
edit_handler = edit_handler_class(instance=page, form=form)
@ -249,6 +268,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
'edit_handler': edit_handler,
'preview_modes': page.preview_modes,
'form': form,
'next': next_url,
})
@ -267,6 +287,8 @@ def edit(request, page_id):
edit_handler_class = page_class.get_edit_handler()
form_class = edit_handler_class.get_form_class(page_class)
next_url = get_valid_next_url_from_request(request)
errors_debug = None
if request.method == 'POST':
@ -399,10 +421,18 @@ def edit(request, page_id):
if is_publishing or is_submitting:
# we're done here - redirect back to the explorer
if next_url:
# redirect back to 'next' url if present
return redirect(next_url)
# redirect back to the explorer
return redirect('wagtailadmin_explore', page.get_parent().id)
else:
# Just saving - remain on edit page for further edits
return redirect('wagtailadmin_pages:edit', page.id)
target_url = reverse('wagtailadmin_pages:edit', args=[page.id])
if next_url:
# Ensure the 'next' url is passed through again if present
target_url += '?next=%s' % urlquote(next_url)
return redirect(target_url)
else:
if page.locked:
messages.error(request, _("The page could not be saved as it is locked"))
@ -433,6 +463,7 @@ def edit(request, page_id):
'errors_debug': errors_debug,
'preview_modes': page.preview_modes,
'form': form,
'next': next_url,
})
@ -441,6 +472,8 @@ def delete(request, page_id):
if not page.permissions_for_user(request.user).can_delete():
raise PermissionDenied
next_url = get_valid_next_url_from_request(request)
if request.method == 'POST':
parent_id = page.get_parent().id
page.delete()
@ -452,11 +485,14 @@ def delete(request, page_id):
if hasattr(result, 'status_code'):
return result
if next_url:
return redirect(next_url)
return redirect('wagtailadmin_explore', parent_id)
return render(request, 'wagtailadmin/pages/confirm_delete.html', {
'page': page,
'descendant_count': page.get_descendant_count()
'descendant_count': page.get_descendant_count(),
'next': next_url,
})
@ -588,9 +624,12 @@ def preview_loading(request):
def unpublish(request, page_id):
page = get_object_or_404(Page, id=page_id).specific
if not page.permissions_for_user(request.user).can_unpublish():
raise PermissionDenied
next_url = get_valid_next_url_from_request(request)
if request.method == 'POST':
page.unpublish()
@ -598,10 +637,13 @@ def unpublish(request, page_id):
messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit'))
])
if next_url:
return redirect(next_url)
return redirect('wagtailadmin_explore', page.get_parent().id)
return render(request, 'wagtailadmin/pages/confirm_unpublish.html', {
'page': page,
'next': next_url,
})
@ -647,7 +689,6 @@ def move_confirm(request, page_to_move_id, destination_id):
if request.method == 'POST':
# any invalid moves *should* be caught by the permission check above,
# so don't bother to catch InvalidMoveToDescendant
page_to_move.move(destination, pos='last-child')
messages.success(request, _("Page '{0}' moved.").format(page_to_move.title), buttons=[
@ -713,6 +754,8 @@ def copy(request, page_id):
# Create the form
form = CopyForm(request.POST or None, page=page, can_publish=can_publish)
next_url = get_valid_next_url_from_request(request)
# Check if user is submitting
if request.method == 'POST':
# Prefill parent_page in case the form is invalid (as prepopulated value for the form field,
@ -753,11 +796,14 @@ def copy(request, page_id):
messages.success(request, _("Page '{0}' copied.").format(page.title))
# Redirect to explore of parent page
if next_url:
return redirect(next_url)
return redirect('wagtailadmin_explore', parent_page.id)
return render(request, 'wagtailadmin/pages/copy.html', {
'page': page,
'form': form,
'next': next_url,
})