How to remove the default delete action in Django admin?
Would the following work?
actions = [ ]
This works:
def get_actions(self, request):
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
It's also the recommended way to do this based off Django's documentation below:
Conditionally enabling or disabling actions
In your admin class, define has_delete_permission to return False:
class YourModelAdmin(admin.ModelAdmin):
...
def has_delete_permission(self, request, obj=None):
return False
Then, it will not show delete button, and will not allow you to delete objects in admin interface.
You can disable "delete selected" action site-wide:
from django.contrib.admin import site
site.disable_action('delete_selected')
When you need to include this action, add 'delete_selected' to the action list:
actions = ['delete_selected']
Documentation
If you want to remove all the action:
class UserAdmin(admin.ModelAdmin):
model = User
actions = None
If you want some specific action:
class UserAdmin(admin.ModelAdmin):
model = User
actions = ['name_of_action_you_want_to_keep']
You can globally disable bulk delete action and enable for selected models only.
Documentation from django website
# Globally disable delete selected
admin.site.disable_action('delete_selected')
# This ModelAdmin will not have delete_selected available
class SomeModelAdmin(admin.ModelAdmin):
actions = ['some_other_action']
...
# This one will
class AnotherModelAdmin(admin.ModelAdmin):
actions = ['delete_selected', 'a_third_action']
...
Giving credit to #DawnTCherian, #tschale and #falsetru
I used:
class YourModelAdmin(admin.ModelAdmin):
...
def get_actions(self, request):
actions = super(YourModelAdmin, self).get_actions(request)
try:
del actions['delete_selected']
except KeyError:
pass
return actions
def has_delete_permission(self, request, obj=None):
return False
It removes the delete action from the list view and the delete option from the detail view.
If you are using that model, as a foreign key in some other model.
Then by using PROTECT constraint for that foreign key you can disable deletion for that model in Django admin.
For Example,
class Exam(models.Model):
student = models.ForeignKey(User, on_delete=models.PROTECT)
marks = models.IntegerField(default=0)
By adding PROTECT constraint to User model through the foreign key present in Exam model, I have disabled the power (in Django admin or elsewhere) to delete students (User) who have written exams.
Related
I'm working on my Django SAAS app in which I want to allow the user to have some custom settings, like disable or enable certain filters. For that I'm using django-user-setttings combined with django-filters and simple forms with boolean fields:
class PropertyFilterSetting(forms.Form):
filter_by_loans = forms.BooleanField(required=False)
filter_by_tenants = forms.BooleanField(required=False)
The issue is that when trying to apply those settings, I keep running into serious spaghetti code:
views.py
class PropertyListView(LoginRequiredMixin, FilterView):
template_name = 'app/property_list.html'
context_object_name = 'properties'
def get_filterset_class(self):
print(get_user_setting('filter_by_tenants', request=self.request))
return PropertyFilterWithoutTenant if not get_user_setting('filter_by_tenants', request=self.request)['value'] else PropertyFilter
filter.py
class PropertyFilter(django_filter.FilterSet):
...
class PropertyFilterWithoutTenant(PropertyFilter):
...
and I'd have to do the same thing with the rest of the features. Is there any better way to implement this?
You can create methods in your User model, or a new class which acts as a store for all the methods. Each method will give you the relevant filterset class based on the value of corresponding user setting.
Something like:
class UserFilterset:
def __init__(self, request):
self.request = request
def get_property_filterset(self):
if not get_user_setting('filter_by_tenants', request=self.request)['value']:
return PropertyFilterWithoutTenant
return PropertyFilter
... # add more such methods for each user setting
Now you can use this method to get the relevant filterset class
class PropertyListView(LoginRequiredMixin, FilterView):
template_name = 'app/property_list.html'
context_object_name = 'properties'
def get_filterset_class(self):
return UserFilterset(self.request).get_property_filterset()
So even if in future you want to add some more logic, you can just update the relevant method, it would be cleaner and manageable.
I'm not sure how MVT stucture will exactly respond to this one but i use a custom generic class in REST structure to add custom filter fields in any viewset that i want
class ListAPIViewWithFilter(ListAPIView):
def get_kwargs_for_filtering(self):
filtering_kwargs = {}
if self.my_filter_fields is not []:
# iterate over the filter fields
for field in self.my_filter_fields:
# get the value of a field from request query parameter
field_value = self.request.query_params.get(field)
if field_value:
filtering_kwargs[field] = field_value
return filtering_kwargs
def get_queryset(self):
queryset = super(ListAPIViewWithFilter, self).get_queryset()
filtering_kwargs = self.get_kwargs_for_filtering()
if filtering_kwargs != {}:
# filter the queryset based on 'filtering_kwargs'
queryset = queryset.filter(**filtering_kwargs)
self.pagination_class = None
else:
return queryset
return queryset[:self.filter_results_number_limit]
changing origional get_queryset function in views.py should be the key to solving your problem (it works in django rest).
try checking what function gets the data then just identify the field wanted from it.
I am upgrading a Django project from Django 1.11. I have successfully upgraded the project upto Django 2.1. When I upgraded to Django 2.2, I got this error message
"(admin.E130) name attributes of actions defined in class AdimClass(not real name) must be unique"
The admins classes are
class AAdmin(admin.ModelAdmin)
def custom_action(self, request, queryset):
# perform custom action
.....
def custom_action_2(self, request, queryset):
# another custom actions
.....
action = [custom_action, custom_action_2]
class BAdmin(AAdmin):
def custom_action(self, request, queryset):
# performs different actions but has the same name as AAdmin action
.....
actions = AAdmin.actions + [custom_action]
problem: (admin.E130) name attributes of actions defined in class AdimClass(not real name) must be unique
If I remove the custom_action from AAdmin, the error is resolved but the action is no more available for other classes which inherits AAdmin.
Goal: keep the action in parent class AAdmin and override it on child class BAdmin.
Note: The code is working fine upto Django 2.1.
The issue is that you are trying to add the same action name "custom_action" to BAdmin twice, the first is inherited by AAdmin. The solution is to not include the duplicate action. A possible solution:
class BAdmin(AAdmin):
def get_actions(self, request):
actions = AAdmin.actions
if 'custom_action' in actions:
del actions['custom_action']
return actions + [custom_action]
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 am new to django and I am a bit confused on how the permission works, or if that is what I am supposed to use in my case.
So, I have my user/model:
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
ROLE_CHOICES = (
(0, ('Student')),
(1, ('Proffesor')),
(2, ('Administration'))
)
role = models.IntegerField(choices=ROLE_CHOICES, default=2)
And then I have my views in election/views.py:
class MainPage(View)
class ElectionList(LoginRequiredMixin, View)
class ElectionDetail(LoginRequiredMixin, View)
#only administration can create elections
class CreateElection(LoginRequiredMixin, CreateView)
How can I restrict a simple user (student, for example) to create an election?
Django already has a Permission and Group models and per-group permissions, so the cleanest "django compatible" way here would be to define "Students", "Professors" and "Administrators" as groups, setup their permissions (eventually adding custom ones if needed), add your users to the appropriate group(s), and test if your current user has the needed permissions for a given action using the permission_required decorator or since you're using a class-based view the PermissionRequiredMixin.
As a side note: since you're using ints for your role values, you may want to add pseudo_constants for them in your model:
class User(AbstractUser):
ROLE_STUDENT = 0
ROLE_PROFESSOR = 1
ROLE_ADMINISTRATOR = 2
ROLE_CHOICES = (
(ROLE_STUDENT, 'Student'),
(ROLE_PROFESSOR, 'Professor'),
(ROLE_ADMINISTRATOR, 'Administration')
)
So you can query / filter your model using sensible human-readable values instead of magic numbers, ie:
students = User.objects.filter(role=User.ROLE_STUDENT)
instead of
students = User.objects.filter(role=0)
But if you use contrib.auth.models.Group for permissions you may not even need this field at all, as you can get your queryset from the groups members.
You can use UserPassesTestMixin
eg.,
class LoginAndPermission(LoginRequiredMixin, UserPassesTestMixin):
def test_func(self):
return self.request.user.is_student
def get_login_url(self):
if self.request.user.is_authenticated():
# User is logged in but does not have permission
return "/permission-denied-url/"
else:
# User is not logged in
return "/login/"
class ElectionDetail(LoginAndPermission, View):
My solution could be an alternative of Django's Decorator.
I'm pretty interesting by your question.
When I have function in my views and I don't want to display this one to a user group, I have a templatetags file :
from django import template
from django.contrib.auth.models import Group
register = template.Library()
#register.filter(name='has_group')
def has_group(user, group_name):
group = Group.objects.get(name=group_name)
return group in user.groups.all()
Then, in my HTML file :
{% if request.user|has_group:"admin" %}
<li>Some part</li>
{% endif %}
I think it's possible in my templatetags to user permission directly in my views.py file, but I don't know How to do that.
Anyway, my method works very well up to now ;)
Read the Permission and Authorization documentation in https://docs.djangoproject.com/en/1.11/topics/auth/default
from django.contrib.auth.mixins import AccessMixin
class AddElectionPermission(AccessMixin):
raise_exception = True
permission_denied_message = 'permission deny'
def dispatch(self, request, *args, **kwargs):
if request.user.role != 0:
return self.handle_no_permission()
return super(AddElectionPermission, self).dispatch(request, *args, **kwargs)
#only administration can create elections
class CreateElection(LoginRequiredMixin, AddElectionPermission, CreateView)
I have 2 models - for example, Book and Page.
Page has a foreign key to Book.
Each page can be marked as "was_read" (boolean), and I want to prevent deleting pages that were read (in the admin).
In the admin - Page is an inline within Book (I don't want Page to be a standalone model in the admin).
My problem - how can I achieve the behavior that a page that was read won't be deleted?
I'm using Django 1.4 and I tried several options:
Override "delete" to throw a ValidationError - the problem is that the admin doesn't "catch" the ValidationError on delete and you get an error page, so this is not a good option.
Override in the PageAdminInline the method - has_delete_permission - the problem here -it's per type so either I allow to delete all pages or I don't.
Are there any other good options without overriding the html code?
Thanks,
Li
The solution is as follows (no HTML code is required):
In admin file, define the following:
from django.forms.models import BaseInlineFormSet
class PageFormSet(BaseInlineFormSet):
def clean(self):
super(PageFormSet, self).clean()
for form in self.forms:
if not hasattr(form, 'cleaned_data'):
continue
data = form.cleaned_data
curr_instance = form.instance
was_read = curr_instance.was_read
if (data.get('DELETE') and was_read):
raise ValidationError('Error')
class PageInline(admin.TabularInline):
model = Page
formset = PageFormSet
You could disable the delete checkbox UI-wise by creating your own custom
formset for the inline model, and set can_delete to False there. For
example:
from django.forms import models
from django.contrib import admin
class MyInline(models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(MyInline, self).__init__(*args, **kwargs)
self.can_delete = False
class InlineOptions(admin.StackedInline):
model = InlineModel
formset = MyInline
class MainOptions(admin.ModelAdmin):
model = MainModel
inlines = [InlineOptions]
Another technique is to disable the DELETE checkbox.
This solution has the benefit of giving visual feedback to the user because she will see a grayed-out checkbox.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
def add_fields(self, form, index):
super().add_fields(form, index)
if some_criteria_to_prevent_deletion:
form.fields['DELETE'].disabled = True
This code leverages the Field.disabled property added in Django 1.9. As the documentation says, "even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data," so you don't need to add more code to prevent deletion.
In your inline, you can add the flag can_delete=False
EG:
class MyInline(admin.TabularInline):
model = models.mymodel
can_delete = False
I found a very easy solution to quietly avoid unwanted deletion of some inlines. You can just override delete_forms property method.
This works not just on admin, but on regular inlines too.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
#property
def deleted_forms(self):
deleted_forms = super(MyInlineFormSet, self).deleted_forms
for i, form in enumerate(deleted_forms):
# Use form.instance to access object instance if needed
if some_criteria_to_prevent_deletion:
deleted_forms.pop(i)
return deleted_forms