I currently have a bunch of inlines that all inherit from a base Inline Class set up like this:
class BaseInlineAdmin(admin.TabularInline):
extra = 0
def has_delete_permission(self, request, obj=None):
return False
def has_add_permission(self, request):
return False
I now want to change my admin so that depending on the user, the inlines change like so:
class PartyAdmin(admin.ModelAdmin):
inline1 = [Inline1, Inline2]
inline2 = [Inline1, Inline2, Inline3]
def get_inline_instances(self, request, obj=None):
if request.user.is_staff:
return [inline(self.model, self.admin_site) for inline in self.inline1]
else:
return [inline(self.model, self.admin_site) for inline in self.inline2]
However, when I do this, the has_add_permission no longer works. When I don't use get_inline_instances, the user has no ability to add another item and I would like this functionality to stay consistent. Is there any reason this is not carrying over when I use this method?
For the record, has_delete_permission remains False which makes the situation even weirder.
get_inline_instances is the method that checks the permission. If you want to check for permissions, you either have to call super().get_inline_instances(), or you need to replicate the code from the original function.
Related
I've set up a custom permissions class to be reused from multiple views, in an app where some users have ownership-like rights on behalf of other users:
class IsOwnerLike(permissions.BasePermission):
def has_permission(self, request, view):
if (
user_is_owner(request.user, request.data["owned_by"])
| user_is_owner_like(request.user, request.data["owned_by"])
):
return True
return False
This works as expected for one ModelViewSet.
However, for legacy reasons, different requests coming in to different views may not have an "owned_by" data element -- it may be called "owned", "owner", "created_by", etc. -- and therefore I can't reuse this custom permission as written.
What is the correct way to abstract things at the viewset, to normalize data being passed to my custom permissions class? Can this be done, or should I be thinking about handling these permissions differently?
Found a solution.
I was able to add a custom attribute to the view:
class ViewOne(ModelViewSet):
ownership_fieldname = "owned_by"
permission_classes = [IsOwnerLike]
...
class ViewTwo(ModelViewSet):
ownership_fieldname = "owner"
permission_classes = [IsOwnerLike]
...
And then access it in the permission:
class IsOwnerLike(permissions.BasePermission):
def has_permission(self, request, view):
if (
user_is_owner(request.user, request.data[view.ownership_fieldname])
| user_is_owner_like(request.user, request.data[view.ownership_fieldname])
):
return True
return False
you should use from has_object_permission for object check for model access permission like this:
def has_object_permission(self, request, view, obj):
if hasattr(obj, 'owner'):
if obj.user == request.user:
return True
return False
you can change owner with your models owner field name
and for pass owner kwargs to permission class you should write custommModelViewSet and inheritance from ModelViewSet and override get_permissions() method
In my models I have Document model with foreign key to the Library model.
When I am in Django admin site I want to disable editing and deleting Library instances when I am creating new Document.
What I tried was to remove delete and edit permissions by subclassing django.contrib.admin.ModelAdmin and removing change/delete permissions
#admin.register(Library)
class LibraryAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
This makes unwanted buttons disappear but also entirely blocks possibility of editing and removing Libraries, which is not what I want. Is there a way to disable these actions only in model edit form?
You could mark the request in the document admin:
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
request._editing_document = object_id is not None # add attribute
return super(DocumentAdmin, self).changeform_view(request, object_id=object_id, form_url=form_url, extra_context=extra_context)
Now you can access that flag in the related admin:
#admin.register(Library)
class LibraryAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
if getattr(request, '_editing_document', False): # query attribute
return False
return super(LibraryAdmin, self).has_delete_permission(request, obj=obj)
Another variation, similar to that of schwobaseggl, would be:
#admin.register(Library)
class LibraryAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
r = super(LibraryAdmin, self).has_delete_permission(request,obj)
if r:
referer = request.path
# Here we can check all the forms were we don`t want to allow Library deletion
if 'documentappname/document/' in referer:
r = False
return r
Pros: you only have to make a function, where you can avoid deleting in many editting pages for different models.
Cons: it relies on the url pattern of your admin app, so, if it changes app or model name (strange but possible) you would have to change it. Another con is that is less fine-grained: you cannot choose to avoid deletion based on some property of the object to be deleted. You coud do this with schwobaseggl's proposal.
I have a DRF ViewSet to which I am adding the CanViewAndEditStaff permission. I want only certain users (user.access_level < 2) to be able to view the list of staff. In my Permissions class, how can I differentiate between a call to the list view and to the get item view. Here is my permissions class:
class CanViewAndEditStaff(permissions.BasePermission):
def has_permission(self, request, view):
# IF THIS IS A LIST VIEW, CHECK ACCESS LEVEL
if ( request.user.access_level < 3 ):
return True
# ELSE, CONTINUE ON TO OBJECT PERMISSIONS
def has_object_permission(self,request,view,account):
# admin can do anything
if ( request.user.access_level == 1 ):
return True
# view/edit/delete
else:
# users can view their own account
if account == request.user:
return True
elif account.access_level >= request.user.access_level:
return True
return False
class CanViewAndEditStaff(permissions.BasePermission):
def has_permission(self, request, view):
# IF THIS IS A LIST VIEW, CHECK ACCESS LEVEL
if (view.action == 'list' and request.user.access_level < 3 ):
return True
# ELSE, CONTINUE ON TO OBJECT PERMISSIONS
you can use view.action to know if this is list or something else.
This doesn't exactly address the question, but this technique is applicable.
I used a variation on Ykh's answer that allows the same permission class to be used broadly across many views which display a variety of different models.
In my view class I added an attribute to distinguish the originating view, thus allowing the appropriate object comparison to determine permissions
# views.py
class SomeView(ListAPIView):
permission_classes = (IsPermd, )
is_some_view = True
class SomeOtherView(RetrieveAPIView
permission_classes = (IsPermd, )
is_some_other_view = True
# permissions.py
class IsPermd(BasePermission):
def has_object_permissions(self, request, view, obj):
if hasattr(view, 'is_some_view'):
# whatever special considerations
if hasattr(view, 'is_some_other_view'):
# whatever other special considerations
This feels a little clunky, but until I find a better way I'll stick with it.
Using Wagtails Modeladmin:
Is there any way to disable edit & delete options leaving only the inspect view?
A possible approach that I can think of, is extending the template, removing the edit & delete buttons and then somehow disable the edit and delete view.
Is there any cleaner approach?
EDIT: Thanks to Loic answer I could figure out.
The PermissionHelper source code was also very helpful to figure out the correct method to override.
Complete answer for only showing inspect view
class ValidationPermissionHelper(PermissionHelper):
def user_can_list(self, user):
return True
def user_can_create(self, user):
return False
def user_can_edit_obj(self, user, obj):
return False
def user_can_delete_obj(self, user, obj):
return False
class ValidationAdmin(ModelAdmin):
model = Validation
permission_helper_class = ValidationPermissionHelper
inspect_view_enabled = True
[...]
Sadly, you need at least one of the add, change or delete permission on that model (set within the roles) for it to show up.
The way around that is to provide a custom permission helper class to your ModelAdmin and always allow listing (and still allow add/change/delete to be set within the roles):
class MyPermissionHelper(wagtail.contrib.modeladmin.helpers.PermissionHelper):
def user_can_list(self, user):
return True # Or any logic related to the user.
class MyModelAdmin(wagtail.contrib.modeladmin.options.ModelAdmin):
model = MyModel
permission_helper_class = MyPermissionHelper
modeladmin_register(wagtail.contrib.modeladmin.options.MyModelAdmin)
class SomeModel(models.Model):
end = models.DateTimeField()
def delete(self, *args, **kwargs):
now = datetime.datetime.now()
if self.end < now:
return # past events cannot be deleted
super(SomeModel, self).delete(self, *args, **kwargs)
I've wrote above code in one of my models.
It's working beautifully but having one single problem:
I'm getting a message saying, object is successfully deleted even if that model is not deleted because if the condition I put in.
Is there a way I can send a message that object is not deleted in this case?
NB: This model is for django-admin only.
The delete view in the django admin does not check to see if the delete() call was successful, so if you want to override the delete method as in your question, you'll need to override the entire ModelAdmin.delete_view method.
If SomeModel is only used in the Django admin, another possible approach is to override the has_delete_permission method. This would remove the delete links from the change view, and disable the delete page for events in the past.
class SomeModelAdmin(admin.ModelAdmin):
...
def has_delete_permission(self, request, obj=None):
"""
Return False for events in the past
"""
if obj is None:
# obj is None in the model admin changelist view
return False
now = datetime.datetime.now()
if obj.end < now:
return False # past events cannot be deleted
else:
return super(SomeModelAdmin, self).has_delete_permission(request, obj)
The implementation above would disable the "delete selected objects" admin action, as we return False when obj is None. You should consider doing this anyway, as it calls the queryset delete method and not your overridden delete method.
With this approach, superadmins would still be able to delete events as they have all permissions. I don't think this approach would work if SomeModel appears in model inlines -- although I see that has_delete_permission is an InlineModelAdmin option in Django 1.4.
You could return True or False from your overridden delete() and just work with the value of that within your form to build your message.
def delete(self, *args, **kwargs):
now = datetime.datetime.now()
if self.end < now:
return False # past events cannot be deleted
super(SomeModel, self).delete(self, *args, **kwargs)
return True #successfully deleted from the database