You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
272 lines
10 KiB
Python
272 lines
10 KiB
Python
import copy
|
|
|
|
from django.contrib import messages
|
|
from django.contrib.auth import get_permission_codename
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.db import models
|
|
from django.http import HttpResponseRedirect
|
|
from django.urls import reverse
|
|
from django.utils.translation import gettext_lazy as _
|
|
from rules.permissions import perm_exists
|
|
|
|
|
|
def decorate_admin_view(model, perm=None):
|
|
"""
|
|
Decorator for wrapping admin views.
|
|
"""
|
|
|
|
def decorator(fun):
|
|
def aux(self, request, object_id):
|
|
try:
|
|
obj = model.objects.get(pk=object_id)
|
|
except model.DoesNotExist:
|
|
messages.error(
|
|
request, _("%(modelname)s not found.") % {"modelname": self.opts.verbose_name}
|
|
)
|
|
return HttpResponseRedirect(
|
|
reverse(
|
|
"admin:{}_{}_changelist".format(self.opts.app_label, self.opts.model_name)
|
|
)
|
|
)
|
|
permitted = (
|
|
self.has_change_permission(request, obj)
|
|
if not perm
|
|
else request.user.has_perm(perm)
|
|
)
|
|
if not permitted:
|
|
messages.error(request, _("Insufficient permissions."))
|
|
return HttpResponseRedirect(
|
|
reverse(
|
|
"admin:{}_{}_changelist".format(self.opts.app_label, self.opts.model_name)
|
|
)
|
|
)
|
|
return fun(self, request, obj)
|
|
|
|
return aux
|
|
|
|
return decorator
|
|
|
|
|
|
class FieldPermissionsAdminMixin:
|
|
field_change_permissions = {}
|
|
field_view_permissions = {}
|
|
|
|
def may_view_field(self, field_desc, request, obj=None):
|
|
if type(field_desc) is not tuple:
|
|
field_desc = (field_desc,)
|
|
for fd in field_desc:
|
|
if fd not in self.field_view_permissions:
|
|
continue
|
|
if not request.user.has_perm(self.field_view_permissions[fd]):
|
|
return False
|
|
return True
|
|
|
|
def get_fieldsets(self, request, obj=None):
|
|
fieldsets = super().get_fieldsets(request, obj)
|
|
d = []
|
|
for title, attrs in fieldsets:
|
|
allowed = [f for f in attrs["fields"] if self.may_view_field(f, request, obj)]
|
|
if len(allowed) == 0:
|
|
continue
|
|
d.append((title, dict(attrs, **{"fields": allowed})))
|
|
return d
|
|
|
|
def get_fields(self, request, obj=None):
|
|
fields = super().get_fields(request, obj)
|
|
return [fd for fd in fields if self.may_view_field(fd, request, obj)]
|
|
|
|
def get_readonly_fields(self, request, obj=None):
|
|
readonly_fields = super().get_readonly_fields(request, obj)
|
|
return list(readonly_fields) + [
|
|
fd
|
|
for fd, perm in self.field_change_permissions.items()
|
|
if not request.user.has_perm(perm)
|
|
]
|
|
|
|
|
|
class ChangeViewAdminMixin:
|
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
|
try:
|
|
return super().change_view(
|
|
request, object_id, form_url=form_url, extra_context=extra_context
|
|
)
|
|
except PermissionDenied:
|
|
opts = self.opts
|
|
obj = self.model.objects.get(pk=object_id)
|
|
messages.error(request, _("You are not allowed to view %(name)s.") % {"name": str(obj)})
|
|
return HttpResponseRedirect(
|
|
reverse("admin:{}_{}_changelist".format(opts.app_label, opts.model_name))
|
|
)
|
|
|
|
|
|
class FilteredQuerysetAdminMixin:
|
|
def get_queryset(self, request):
|
|
"""
|
|
Return a QuerySet of all model instances that can be edited by the
|
|
admin site. This is used by changelist_view.
|
|
"""
|
|
qs = self.model._default_manager.get_queryset()
|
|
ordering = self.get_ordering(request)
|
|
if ordering:
|
|
qs = qs.order_by(*ordering)
|
|
queryset = qs
|
|
list_global_perm = "{}.list_global_{}".format(self.opts.app_label, self.opts.model_name)
|
|
if request.user.has_perm(list_global_perm):
|
|
view_global_perm = "{}.view_global_{}".format(self.opts.app_label, self.opts.model_name)
|
|
if request.user.has_perm(view_global_perm):
|
|
return queryset
|
|
if hasattr(request.user, "member"):
|
|
return request.user.member.annotate_view_permission(queryset, model=self.model)
|
|
return queryset.annotate(_viewable=models.Value(False))
|
|
|
|
if not hasattr(request.user, "member"):
|
|
return self.model.objects.none()
|
|
|
|
return request.user.member.filter_queryset_by_permissions(
|
|
queryset, annotate=True, model=self.model
|
|
)
|
|
|
|
|
|
# class ObjectPermissionsInlineModelAdminMixin(rules.contrib.admin.ObjectPermissionsInlineModelAdminMixin):
|
|
|
|
|
|
class CommonAdminMixin(
|
|
FieldPermissionsAdminMixin, ChangeViewAdminMixin, FilteredQuerysetAdminMixin
|
|
):
|
|
def has_add_permission(self, request, obj=None):
|
|
assert obj is None
|
|
opts = self.opts
|
|
codename = get_permission_codename("add_global", opts)
|
|
perm = "{}.{}".format(opts.app_label, codename)
|
|
return request.user.has_perm(perm, obj)
|
|
|
|
def has_view_permission(self, request, obj=None):
|
|
opts = self.opts
|
|
if obj is None:
|
|
codename = get_permission_codename("view", opts)
|
|
else:
|
|
codename = get_permission_codename("view_obj", opts)
|
|
perm = "{}.{}".format(opts.app_label, codename)
|
|
if perm_exists(perm):
|
|
return request.user.has_perm(perm, obj)
|
|
else:
|
|
return self.has_change_permission(request, obj)
|
|
|
|
def has_change_permission(self, request, obj=None):
|
|
opts = self.opts
|
|
if obj is None:
|
|
codename = get_permission_codename("view", opts)
|
|
else:
|
|
codename = get_permission_codename("change_obj", opts)
|
|
return request.user.has_perm("{}.{}".format(opts.app_label, codename), obj)
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
opts = self.opts
|
|
if obj is None:
|
|
codename = get_permission_codename("delete_global", opts)
|
|
else:
|
|
codename = get_permission_codename("delete_obj", opts)
|
|
return request.user.has_perm("{}.{}".format(opts.app_label, codename), obj)
|
|
|
|
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
"""
|
|
COPIED from django to disable related actions
|
|
|
|
Hook for specifying the form Field instance for a given database Field
|
|
instance.
|
|
|
|
If kwargs are given, they're passed to the form Field's constructor.
|
|
"""
|
|
# If the field specifies choices, we don't need to look for special
|
|
# admin widgets - we just need to use a select widget of some kind.
|
|
if db_field.choices:
|
|
return self.formfield_for_choice_field(db_field, request, **kwargs)
|
|
|
|
# ForeignKey or ManyToManyFields
|
|
if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
|
|
# Combine the field kwargs with any options for formfield_overrides.
|
|
# Make sure the passed in **kwargs override anything in
|
|
# formfield_overrides because **kwargs is more specific, and should
|
|
# always win.
|
|
if db_field.__class__ in self.formfield_overrides:
|
|
kwargs = {**self.formfield_overrides[db_field.__class__], **kwargs}
|
|
|
|
# Get the correct formfield.
|
|
if isinstance(db_field, models.ForeignKey):
|
|
formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
|
|
elif isinstance(db_field, models.ManyToManyField):
|
|
formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
|
|
|
|
# For non-raw_id fields, wrap the widget with a wrapper that adds
|
|
# extra HTML -- the "add other" interface -- to the end of the
|
|
# rendered output. formfield can be None if it came from a
|
|
# OneToOneField with parent_link=True or a M2M intermediary.
|
|
# if formfield and db_field.name not in self.raw_id_fields:
|
|
# formfield.widget = widgets.RelatedFieldWidgetWrapper(
|
|
# formfield.widget,
|
|
# db_field.remote_field,
|
|
# self.admin_site,
|
|
# )
|
|
|
|
return formfield
|
|
|
|
# If we've got overrides for the formfield defined, use 'em. **kwargs
|
|
# passed to formfield_for_dbfield override the defaults.
|
|
for klass in db_field.__class__.mro():
|
|
if klass in self.formfield_overrides:
|
|
kwargs = {**copy.deepcopy(self.formfield_overrides[klass]), **kwargs}
|
|
return db_field.formfield(**kwargs)
|
|
|
|
# For any other type of field, just call its formfield() method.
|
|
return db_field.formfield(**kwargs)
|
|
|
|
|
|
class CommonAdminInlineMixin(CommonAdminMixin):
|
|
def has_add_permission(self, request, obj):
|
|
# assert obj is not None
|
|
if obj is None:
|
|
return True
|
|
if obj.pk is None:
|
|
return True
|
|
codename = get_permission_codename("add_obj", self.opts)
|
|
return request.user.has_perm("{}.{}".format(self.opts.app_label, codename), obj)
|
|
|
|
def has_view_permission(self, request, obj=None): # pragma: no cover
|
|
if obj is None:
|
|
return True
|
|
if obj.pk is None:
|
|
return True
|
|
opts = self.opts
|
|
if obj is None:
|
|
codename = get_permission_codename("view", opts)
|
|
else:
|
|
codename = get_permission_codename("view_obj", opts)
|
|
perm = "{}.{}".format(opts.app_label, codename)
|
|
if perm_exists(perm):
|
|
return request.user.has_perm(perm, obj)
|
|
else:
|
|
return self.has_change_permission(request, obj)
|
|
|
|
def has_change_permission(self, request, obj=None): # pragma: no cover
|
|
if obj is None:
|
|
return True
|
|
if obj.pk is None:
|
|
return True
|
|
opts = self.opts
|
|
if opts.auto_created:
|
|
for field in opts.fields:
|
|
if field.rel and field.rel.to != self.parent_model:
|
|
opts = field.rel.to._meta
|
|
break
|
|
codename = get_permission_codename("change_obj", opts)
|
|
return request.user.has_perm("{}.{}".format(opts.app_label, codename), obj)
|
|
|
|
def has_delete_permission(self, request, obj=None): # pragma: no cover
|
|
if obj is None:
|
|
return True
|
|
if obj.pk is None:
|
|
return True
|
|
if self.opts.auto_created:
|
|
return self.has_change_permission(request, obj)
|
|
return super().has_delete_permission(request, obj)
|