mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-04-09 09:30:59 +00:00
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:
parent
35a1af2b3b
commit
37bbbb9dba
9 changed files with 477 additions and 464 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
3
wagtail/contrib/modeladmin/helpers/__init__.py
Normal file
3
wagtail/contrib/modeladmin/helpers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .button import ButtonHelper, PageButtonHelper # NOQA
|
||||
from .permission import PagePermissionHelper, PermissionHelper # NOQA
|
||||
from .url import AdminURLHelper, PageAdminURLHelper # NOQA
|
||||
183
wagtail/contrib/modeladmin/helpers/button.py
Normal file
183
wagtail/contrib/modeladmin/helpers/button.py
Normal 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
|
||||
167
wagtail/contrib/modeladmin/helpers/permission.py
Normal file
167
wagtail/contrib/modeladmin/helpers/permission.py
Normal 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()
|
||||
56
wagtail/contrib/modeladmin/helpers/url.py
Normal file
56
wagtail/contrib/modeladmin/helpers/url.py
Normal 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)
|
||||
56
wagtail/contrib/modeladmin/mixins.py
Normal file
56
wagtail/contrib/modeladmin/mixins.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue