diff --git a/docs/reference/contrib/modeladmin/indexview.rst b/docs/reference/contrib/modeladmin/indexview.rst index db68350dd..e5506b3cc 100644 --- a/docs/reference/contrib/modeladmin/indexview.rst +++ b/docs/reference/contrib/modeladmin/indexview.rst @@ -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) diff --git a/docs/reference/contrib/modeladmin/primer.rst b/docs/reference/contrib/modeladmin/primer.rst index 748a95479..2bcbe7b01 100644 --- a/docs/reference/contrib/modeladmin/primer.rst +++ b/docs/reference/contrib/modeladmin/primer.rst @@ -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 diff --git a/wagtail/contrib/modeladmin/helpers.py b/wagtail/contrib/modeladmin/helpers.py deleted file mode 100644 index 9e0036403..000000000 --- a/wagtail/contrib/modeladmin/helpers.py +++ /dev/null @@ -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[-\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 diff --git a/wagtail/contrib/modeladmin/helpers/__init__.py b/wagtail/contrib/modeladmin/helpers/__init__.py new file mode 100644 index 000000000..d7119085d --- /dev/null +++ b/wagtail/contrib/modeladmin/helpers/__init__.py @@ -0,0 +1,3 @@ +from .button import ButtonHelper, PageButtonHelper # NOQA +from .permission import PagePermissionHelper, PermissionHelper # NOQA +from .url import AdminURLHelper, PageAdminURLHelper # NOQA diff --git a/wagtail/contrib/modeladmin/helpers/button.py b/wagtail/contrib/modeladmin/helpers/button.py new file mode 100644 index 000000000..6d90e0f41 --- /dev/null +++ b/wagtail/contrib/modeladmin/helpers/button.py @@ -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 diff --git a/wagtail/contrib/modeladmin/helpers/permission.py b/wagtail/contrib/modeladmin/helpers/permission.py new file mode 100644 index 000000000..7444f6d71 --- /dev/null +++ b/wagtail/contrib/modeladmin/helpers/permission.py @@ -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() diff --git a/wagtail/contrib/modeladmin/helpers/url.py b/wagtail/contrib/modeladmin/helpers/url.py new file mode 100644 index 000000000..42f51bf25 --- /dev/null +++ b/wagtail/contrib/modeladmin/helpers/url.py @@ -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[-\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) diff --git a/wagtail/contrib/modeladmin/mixins.py b/wagtail/contrib/modeladmin/mixins.py new file mode 100644 index 000000000..e73f6f9f1 --- /dev/null +++ b/wagtail/contrib/modeladmin/mixins.py @@ -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(''.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(''.format(flatatt(img_attrs))) + admin_thumb.short_description = thumb_col_header_text diff --git a/wagtail/contrib/modeladmin/options.py b/wagtail/contrib/modeladmin/options.py index cb77737cc..28dca98c8 100644 --- a/wagtail/contrib/modeladmin/options.py +++ b/wagtail/contrib/modeladmin/options.py @@ -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(''.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(''.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