kopia lustrzana https://github.com/wagtail/wagtail
Added wagtailmodeladmin to contrib with docs
rodzic
a31d4d32ce
commit
dcb67dcfdf
|
@ -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)
|
||||
|
|
|
@ -122,6 +122,7 @@ Contributors
|
|||
* Janneke Janssen
|
||||
* Roel Bruggink
|
||||
* Yannick Chabbert
|
||||
* Andy Babic
|
||||
|
||||
Translators
|
||||
===========
|
||||
|
|
|
@ -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".
|
||||
|
|
|
@ -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 doesn’t need to extend ``Page`` or
|
||||
be registered as a ``Snippet``), and it won’t 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>`_.
|
||||
|
|
@ -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"
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
static/
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -0,0 +1,6 @@
|
|||
#id_parent_page li {
|
||||
margin: 15px 0;
|
||||
}
|
||||
#id_parent_page li label {
|
||||
float: none;
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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 %}
|
||||
|
||||
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 %}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{% load modeladmin_tags %}
|
||||
{% for item in result %}
|
||||
{% result_row_value_display forloop.counter0 %}
|
||||
{% endfor %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
||||
|
|
@ -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(' ')
|
||||
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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
|
@ -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"
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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
|
|
@ -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)
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue