Improved code separation in contrib.modeladmin (#3467)

* Split `helpers.py` into separate `url.py`, `permission.py` and `button.py`, dedicated to those separate concerns and update the docs accordingly

* Move `ThumbnailMixin` out to `mixins.py` and update documentation accordingly

* Ad #NOQA to import lines to hush pep errors

* Alphabetise helper import order

* - Delete `helpers/helpers.py`
- wagtal -> wagtail in docs
This commit is contained in:
Andy Babic 2017-03-28 10:34:03 +01:00 committed by Matt Westcott
parent 35a1af2b3b
commit 37bbbb9dba
9 changed files with 477 additions and 464 deletions

View file

@ -535,7 +535,7 @@ kind of interactivity using javascript:
.. _modeladmin_thumbnailmixin:
----------------------------------------------------
``wagtal.contrib.modeladmin.options.ThumbnailMixin``
``wagtail.contrib.modeladmin.mixins.ThumbnailMixin``
----------------------------------------------------
If you're using ``wagtailimages.Image`` to define an image for each item in
@ -547,7 +547,8 @@ change a few attributes to change the thumbnail to your liking, like so:
.. code-block:: python
from django.db import models
from wagtail.contrib.modeladmin.options import ThumbnailMixin, ModelAdmin
from wagtail.contrib.modeladmin.mixins import ThumbnailMixin
from wagtail.contrib.modeladmin.options import ModelAdmin
class Person(models.Model):
name = models.CharField(max_length=255)

View file

@ -250,9 +250,9 @@ by setting values on the following attributes:
``ModelAdmin.url_helper_class``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, the ``modeladmin.helpers.PageAdminURLHelper`` class is used when
your model extends ``wagtailcore.models.Page``, otherwise
``modeladmin.helpers.AdminURLHelper`` is used.
By default, the ``modeladmin.helpers.url.PageAdminURLHelper`` class is used
when your model extends ``wagtailcore.models.Page``, otherwise
``modeladmin.helpers.url.AdminURLHelper`` is used.
If you find that the above helper classes don't cater for your needs, you can
easily create your own helper class, by sub-classing ``AdminURLHelper`` or
@ -301,9 +301,9 @@ so:
``ModelAdmin.permission_helper_class``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, the ``modeladmin.helpers.PagePermissionHelper``
By default, the ``modeladmin.helpers.permission.PagePermissionHelper``
class is used when your model extends ``wagtailcore.models.Page``,
otherwise ``wagtail.contrib.modeladmin.helpers.PermissionHelper`` is used.
otherwise ``modeladmin.helpers.permission.PermissionHelper`` is used.
If you find that the above helper classes don't cater for your needs, you can
easily create your own helper class, by sub-classing
@ -350,9 +350,9 @@ isn't possible or doesn't meet your needs, you can override the
``ModelAdmin.button_helper_class``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, the ``modeladmin.helpers.PageButtonHelper`` class is used when your
model extends ``wagtailcore.models.Page``, otherwise
``modeladmin.helpers.ButtonHelper`` is used.
By default, the ``modeladmin.helpers.button.PageButtonHelper`` class is used
when your model extends ``wagtailcore.models.Page``, otherwise
``modeladmin.helpers.button.ButtonHelper`` is used.
If you wish to add or change buttons for your model's IndexView, you'll need to
create your own button helper class, by sub-classing ``ButtonHelper`` or (if

View file

@ -1,402 +0,0 @@
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, UserPagePermissionsProxy
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_add=None,
classnames_exclude=None):
if exclude is None:
exclude = []
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_add=None,
classnames_exclude=None):
if exclude is None:
exclude = []
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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

View file

@ -0,0 +1,3 @@
from .button import ButtonHelper, PageButtonHelper # NOQA
from .permission import PagePermissionHelper, PermissionHelper # NOQA
from .url import AdminURLHelper, PageAdminURLHelper # NOQA

View file

@ -0,0 +1,183 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.admin.utils import quote
from django.utils.encoding import force_text
from django.utils.translation import ugettext as _
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_add=None,
classnames_exclude=None):
if exclude is None:
exclude = []
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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=None, classnames_add=None,
classnames_exclude=None):
if exclude is None:
exclude = []
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
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

View file

@ -0,0 +1,167 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.auth import get_permission_codename
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from wagtail.wagtailcore.models import Page, UserPagePermissionsProxy
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()

View file

@ -0,0 +1,56 @@
from __future__ import absolute_import, unicode_literals
from django.core.urlresolvers import reverse
from django.utils.functional import cached_property
from django.utils.http import urlquote
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)

View file

@ -0,0 +1,56 @@
from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.forms.utils import flatatt
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
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 __init__(self, *args, **kwargs):
if 'wagtail.wagtailimages' not in settings.INSTALLED_APPS:
raise ImproperlyConfigured(
u"The `wagtail.wagtailimages` app must be installed in order "
"to use the `ThumbnailMixin` class."
)
super(ThumbnailMixin, self).__init__(*args, **kwargs)
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 not image:
if self.thumb_default:
return mark_safe('<img{}>'.format(flatatt(img_attrs)))
return ''
# try to get a rendition of the image to use
from wagtail.wagtailimages.shortcuts import get_rendition_or_not_found
spec = self.thumb_image_filter_spec
rendition = get_rendition_or_not_found(image, spec)
img_attrs.update({'src': rendition.url})
return mark_safe('<img{}>'.format(flatatt(img_attrs)))
admin_thumb.short_description = thumb_col_header_text

View file

@ -1,13 +1,10 @@
from __future__ import absolute_import, unicode_literals
from django.conf import settings
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.utils 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
@ -16,6 +13,7 @@ from .helpers import (
AdminURLHelper, ButtonHelper, PageAdminURLHelper, PageButtonHelper, PagePermissionHelper,
PermissionHelper)
from .menus import GroupMenuItem, ModelAdminMenuItem, SubMenu
from .mixins import ThumbnailMixin # NOQA
from .views import ChooseParentView, CreateView, DeleteView, EditView, IndexView, InspectView
@ -59,55 +57,6 @@ class WagtailRegisterable(object):
return False
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 __init__(self, *args, **kwargs):
if 'wagtail.wagtailimages' not in settings.INSTALLED_APPS:
raise ImproperlyConfigured(
u"The `wagtail.wagtailimages` app must be installed in order "
"to use the `ThumbnailMixin` class."
)
super(ThumbnailMixin, self).__init__(*args, **kwargs)
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 not image:
if self.thumb_default:
return mark_safe('<img{}>'.format(flatatt(img_attrs)))
return ''
# try to get a rendition of the image to use
from wagtail.wagtailimages.shortcuts import get_rendition_or_not_found
spec = self.thumb_image_filter_spec
rendition = get_rendition_or_not_found(image, spec)
img_attrs.update({'src': rendition.url})
return mark_safe('<img{}>'.format(flatatt(img_attrs)))
admin_thumb.short_description = thumb_col_header_text
class ModelAdmin(WagtailRegisterable):
"""
The core modeladmin class. It provides an alternative means to