The default "delete selected" admin action in Django - django

How can I remove or change the verbose name of the default admin action "delete selected X item" in the Django admin panel?

Alternatively to Googol's solution, and by waiting for delete_model() to be implemented in current Django version , I suggest the following code.
It disables the default delete action for current AdminForm only.
class FlowAdmin(admin.ModelAdmin):
actions = ['delete_model']
def get_actions(self, request):
actions = super(MyModelAdmin, self).get_actions(request)
del actions['delete_selected']
return actions
def delete_model(self, request, obj):
for o in obj.all():
o.delete()
delete_model.short_description = 'Delete flow'
admin.site.register(Flow, FlowAdmin)

You can disable the action from appearing with this code.
from django.contrib import admin
admin.site.disable_action('delete_selected')
If you chose, you could then restore it on individual models with this:
class FooAdmin(admin.ModelAdmin):
actions = ['my_action', 'my_other_action', admin.actions.delete_selected]

Not sure if this sort of monkey-patching is a good idea, but shoving this in one of my admin.py works for me:
from django.contrib.admin.actions import delete_selected
delete_selected.short_description = u'How\'s this for a name?'
This will change the verbose name for all your admin sites. If you want to change it just for one particular model's admin, I think you'll need to write a custom admin action.
Tested with Django version 1.1:
>>> import django
>>> django.VERSION
(1, 1, 0, 'beta', 1)

For globally changing delete_selected's short_description Dominic Rodger's answer seems best.
However for changing the short_description on the admin for a single model I think this alternative to Stéphane's answer is better:
def get_actions(self, request):
actions = super().get_actions(request)
actions['delete_selected'][0].short_description = "Delete Selected"
return actions

In order to replace delete_selected I do the following:
Copy the function delete_selected from contrib/admin/actions.py to your admin.py and rename it. Also copy the template contrib/admin/templates/delete_selected_confirmation.html to your template directory and rename it. Mine looks like this:
def reservation_bulk_delete(modeladmin, request, queryset):
"""
Default action which deletes the selected objects.
This action first displays a confirmation page whichs shows all the
deleteable objects, or, if the user has no permission one of the related
childs (foreignkeys), a "permission denied" message.
Next, it delets all selected objects and redirects back to the change list.
"""
opts = modeladmin.model._meta
app_label = opts.app_label
# Check that the user has delete permission for the actual model
if not modeladmin.has_delete_permission(request):
raise PermissionDenied
# Populate deletable_objects, a data structure of all related objects that
# will also be deleted.
# deletable_objects must be a list if we want to use '|unordered_list' in the template
deletable_objects = []
perms_needed = set()
i = 0
for obj in queryset:
deletable_objects.append([mark_safe(u'%s: %s' % (escape(force_unicode(capfirst(opts.verbose_name))), obj.pk, escape(obj))), []])
get_deleted_objects(deletable_objects[i], perms_needed, request.user, obj, opts, 1, modeladmin.admin_site, levels_to_root=2)
i=i+1
# The user has already confirmed the deletion.
# Do the deletion and return a None to display the change list view again.
if request.POST.get('post'):
if perms_needed:
raise PermissionDenied
n = queryset.count()
if n:
for obj in queryset:
obj_display = force_unicode(obj)
obj.delete()
modeladmin.log_deletion(request, obj, obj_display)
#queryset.delete()
modeladmin.message_user(request, _("Successfully deleted %(count)d %(items)s.") % {
"count": n, "items": model_ngettext(modeladmin.opts, n)
})
# Return None to display the change list page again.
return None
context = {
"title": _("Are you sure?"),
"object_name": force_unicode(opts.verbose_name),
"deletable_objects": deletable_objects,
'queryset': queryset,
"perms_lacking": perms_needed,
"opts": opts,
"root_path": modeladmin.admin_site.root_path,
"app_label": app_label,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
}
# Display the confirmation page
return render_to_response(modeladmin.delete_confirmation_template or [
"admin/%s/%s/reservation_bulk_delete_confirmation.html" % (app_label, opts.object_name.lower()),
"admin/%s/reservation_bulk_delete_confirmation.html" % app_label,
"admin/reservation_bulk_delete_confirmation.html"
], context, context_instance=template.RequestContext(request))
As you can see I commented out
queryset.delete()
and rather use:
obj.delete()
That's not optimal yet - you should apply something to the entire queryset for better performance.
In admin.py I disable the default action delete_selected for the entire admin site:
admin.site.disable_action('delete_selected')
Instead I use my own function where needed:
class ReservationAdmin(admin.ModelAdmin):
actions = [reservation_bulk_delete, ]
In my model I define the delete() function:
class Reservation(models.Model):
def delete(self):
self.status_server = RESERVATION_STATUS_DELETED
self.save()

http://docs.djangoproject.com/en/dev/ref/contrib/admin/actions/#disabling-a-site-wide-action

from django.contrib.admin import sites
from django.contrib.admin.actions import delete_selected
class AdminSite(sites.AdminSite):
"""
Represents the administration, where only authorized users have access.
"""
def __init__(self, *args, **kwargs):
super(AdminSite, self).__init__(*args, **kwargs)
self.disable_action('delete_selected')
self.add_action(self._delete_selected, 'delete_selected')
#staticmethod
def _delete_selected(modeladmin, request, queryset):
_delete_qs = queryset.delete
def delete():
for obj in queryset:
modeladmin.delete_model(request, obj)
_delete_qs()
queryset.delete = delete
return delete_selected(modeladmin, request, queryset)

class FooAdmin(sites.AdminSite):
not_deleted = ['value1', 'value2']
actions = ['delete_selected_values']
def delete_selected_values(self, request, queryset):
# my custom logic
exist = queryset.filter(value__in=self.not_deleted).exists()
if exist:
error_message = "Error"
self.message_user(request, error_message, level=messages.ERROR)
else:
delete_action = super().get_action('delete_selected')[0]
return delete_action(self, request, queryset)
delete_selected_values.short_description = 'delete selected'
admin.site.register(Foo, FooAdmin)

Related

Is there a way to override the delete_selected method in ModelAdmin but keep confirmation?

I have:
class Person(admin.ModelAdmin):
actions = ['delete_selected']
def delete_selected(modeladmin, request, queryset):
# Show confirmation page.
for obj in queryset:
obj.custom_delete()
That comment I left there is where I'm struggling. I still want to show the confirmation page before I perform my custom delete.
Short answer: you should override delete_queryset [Django-doc], since this encapsulates the real logic to remove the objects.
You should not override delete_selected. This action is defined like [GitHub]:
def delete_selected(modeladmin, request, queryset):
# ...
# Populate deletable_objects, a data structure of all related objects that
# will also be deleted.
deletable_objects, model_count, perms_needed, protected = modeladmin.get_deleted_objects(queryset, request)
# The user has already confirmed the deletion.
# Do the deletion and return None to display the change list view again.
if request.POST.get('post') and not protected:
if perms_needed:
raise PermissionDenied
n = queryset.count()
if n:
for obj in queryset:
obj_display = str(obj)
modeladmin.log_deletion(request, obj, obj_display)
modeladmin.delete_queryset(request, queryset)
modeladmin.message_user(request, _("Successfully deleted %(count)d %(items)s.") % {
"count": n, "items": model_ngettext(modeladmin.opts, n)
}, messages.SUCCESS)
# Return None to display the change list page again.
return None
# ...
context = {
# ...
}
request.current_app = modeladmin.admin_site.name
# Display the confirmation page
return TemplateResponse(request, modeladmin.delete_selected_confirmation_template or [
"admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.model_name),
"admin/%s/delete_selected_confirmation.html" % app_label,
"admin/delete_selected_confirmation.html"
], context)
delete_selected.allowed_permissions = ('delete',)
delete_selected.short_description = gettext_lazy("Delete selected %(verbose_name_plural)s")
The key part here is that this action will perform the proper checks, but the deletion itself is done through a call:
modeladmin.delete_queryset(request, queryset)
So it is sufficient to override delete_queryset instead, with:
class PersonAdmin(admin.ModelAdmin):
actions = ['delete_selected']
def delete_queryset(self, request, queryset):
for obj in queryset:
obj.custom_delete()
A ModelAdmin has a standard implementation for delete_queryset [GitHub]:
class ModelAdmin(BaseModelAdmin):
# ...
def delete_queryset(self, request, queryset):
"""Given a queryset, delete it from the database."""
queryset.delete()
My requirement was a bit different. I had to intercept the delete_selected action and display error depending on the condition. This is what I did -
In Model Admin
#admin.register(Model)
class ModelAdmin(model.Admin):
...
def get_actions(self, request):
actions = super().get_actions(request)
self.actions.append(delete_selected)
self.actions.append(...otheractions)
return actions
Outside the Model admin
#Import goes to top of the file
from django.contrib.admin.actions import delete_selected as default_delete_selected
def delete_selected(modeladmin, request, queryset):
response = HttpResponseRedirect(request.get_full_path())
#logic to validate
for obj in queryset:
if obj.name == 'test':
messages.error(request,'error message')
return response
return default_delete_selected(modeladmin, request, queryset)

django-rest save array of data to db

hi im trying to save a form data into db.
i provided print of requset.data for you as you see requirement have two items.
i want to save each item in database i used for loop to save each item of list but the loop will save each character of item like h-e-l,... in table row...
where is my mistake ... thanks
also print of request.data.get('requirement') will retun second item
this is print of request.data in sever:
<QueryDict: {'requirement': ['hello', 'bye'], 'audience': ['adasd'], 'achievement': ['asdasd'], 'section': ['410101010'], 'title': ['asdasd'], 'mini_description': ['asdad'], 'full_description': ['asdasd'], 'video_length': ['10101'], 'video_level': ['P'], 'price': [''], 'free': ['true'], 'image': [<InMemoryUploadedFile: p.gif (image/gif)>]}>
view:
class StoreCreateAPIView(generics.CreateAPIView):
parser_classes = (MultiPartParser, FormParser)
permission_classes = [IsAuthenticated]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def post(self, request, *args, **kwargs):
if request.method == 'POST':
print(request.data)
file_serial = ProductSerializer(data=request.data, context={"request": request})
if file_serial.is_valid():
file_serial.save(author_id=request.user.id)
requirement = request.data['requirement']
audience = request.data.get('audience')
achievement = request.data.get('achievement')
sections = request.data.get('section')
print(request.data['requirement'])
pid = file_serial.data.get('product_id')
for item in requirement :
req = ProductRequiredItems(
item = item,
product_id = pid
)
req.save()
First of all, overriding CreateAPIView's post method in your code makes your custom perform_create method useless, unless you explicitly call it from within your customized post method. Otherwise it will never be called.
also print of request.data.get('requirement') will retun second item
It does return the last item as per Django docs for QueryDict.__getitem__(key).
i want to save each item in database i used for loop to save each item of list but the loop will save each character of item like h-e-l,...
This is because of the above functionality of QueryDict. When you do:
requirement = request.data['requirement']
# requirement = request.__getitem__('requirement')
it will call QueryDict.__getitem__(key) method and thus return only the last item (which is string in you example).
Answer:
You can simply override CreateAPIView's create method, and let your serializer handle all the rest.
# views.py
from django.shortcuts import render
from rest_framework import generics, status
from rest_framework.response import Response
from .models import MyObj
from .serializers import MyObjSerializer
class MyObjView(generics.CreateAPIView):
serializer_class = MyObjSerializer
queryset = MyObj.objects.all()
def create(self, request, *args, **kwargs):
# The QueryDicts at request.POST and request.GET will be immutable
# when accessed in a normal request/response cycle.
# To get a mutable version you need to use QueryDict.copy().
req_data = request.data.copy()
requirements = req_data.pop('requirement')
serializers_data = []
for requirement in requirements:
req_data ['requirement'] = requirement
serializer = self.get_serializer(data=req_data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
serializers_data.append(serializer.data)
return Response(serializers_data, status=status.HTTP_201_CREATED)
# serializers.py
from rest_framework import serializers
from .models import MyObj
class MyObjSerializer(serializers.ModelSerializer):
class Meta:
model = MyObj
fields = '__all__'
Have a look at DRF CreateModelMixin. It defines create & perform_create methods that are used used in CreateAPIView upon executing POST request. I just altered them slightly to handle your specific case.
Hope it helps.

Django Admin prevent delete on last record

I'm new to django and learning as I go.
I have a model that must have 1 record, therefore I want to prevent delete for the last record.
model.py
class SliderContent (models.Model):
name = models.CharField(max_length=40)
image = models.ImageField(upload_to='image')
I tried the following in admin.py
class SliderContentAdmin(admin.ModelAdmin):
.....
def delete_model(self, request, obj):
if last_record:
storage = messages.get_messages(request)
storage.used = True
messages.error(request, 'Cannot delete last record.')
else:
obj.delete()
This didn't work, I have also looked at a pre_delete receiver but from what I have read this won't work as I'm using the build in Admin views.
What is the best way to do this?
Override the has_delete_permission() method:
from django.contrib.admin import helpers
class SliderContentAdmin(admin.ModelAdmin):
min_objects = 1
def has_delete_permission(self, request, obj):
queryset = self.model.objects.all()
# If we're running the bulk delete action, estimate the number
# of objects after we delete the selected items
selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
if selected:
queryset = queryset.exclude(pk__in=selected)
if queryset.count() <= self.min_objects:
message = 'There should be at least {} object(s) left.'
self.message_user(request, message.format(self.min_objects))
return False
return super(SliderContentAdmin, self).has_delete_permission(request, obj)

Allowing only some given instances of a model to be deleted from the admin

in the admin of a model I would like to allow the delete action only for some of the instances (my model has a DateTimeField and I would like to disable the delete action for instances which have this field set to the current month).
Anybody could help?
Thanks
EDIT
I tried the method proposed by Chris in his anser below, but obj is always None:
class UserProfileAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
# obj is always None
return super(UserProfileAdmin, self).has_delete_permission(request, obj=obj)
class MyModelAdmin(admin.ModelAdmin):
...
def has_delete_permission(self, request, obj=None):
if obj is not None and \
obj.my_date_field.month == datetime.now().month and \
obj.my_date_field.year == datetime.now().year:
return False
return super(MyModelAdmin, self).has_delete_permission(request, obj=obj)
UPDATE:
It's not "always None", it's set to a specific object when a specific object can be ascertained. In the changelist, and particularly in your scenario when trying to bulk-delete from the changelist, it's set to None because no individual object can obviously be determined.
If you need to account for deletion from the changelist, you'll have to create your own delete action and replace the default Django version. Something like:
class MyModelAdmin(admin.ModelAdmin):
...
actions = ['limited_delete_selected']
# Need to remove the default delete_selected action
def get_actions(self, request):
actions = super(MyModelAdmin, self).get_actions(request)
if actions.has_key('delete_selected'):
del actions['delete_selected']
return actions
def limited_delete_selected(self, request, queryset):
# filter selected items to only those that are actually deletable
now = datetime.now()
queryset = queryset.exclude(date_field__month=now.month, date_field__year=now.year)
# call Django's delete_selected with limited queryset
from django.contrib.admin.actions import delete_selected
delete_selected(self, request, queryset)
limited_delete_selected.short_description = "Delete selected objects or whatever you want it to say"
You actually will need both the action and the original has_delete_permission since objects can be deleted individually on their change_form view.
Check out the docs around overriding built-in model methods: https://docs.djangoproject.com/en/1.3/topics/db/models/#overriding-model-methods

Defining a custom app_list in django admin index page

I'd like to define a custom application list to use in django's admin index page because I want the apps displayed in a specific order, rather than the default alphabetical order. Trawling through various SO posts it would appear that it's not yet possible to declare the desired application order in any of the obvious places (e.g. admin.py, models.py).
Now, I can see that the django admin's index.html file contains the following statement:
{% for app in app_list %}
# do stuff with the app object
So I'd like to change this to use a custom list object called, say, my_app_list. In python I'd do this along the following lines:
from django.db.models import get_app
my_app_list = [get_app('myapp1'), get_app('myapp2'), ..., get_app('django.contrib.auth')]
for app in my_app_list
...
My question then is, how do I code the equivalent of the first 2 lines above into my local copy of the index.html file?
Or, alternatively, what python source file should I insert those lines into such that the variable my_app_list is available within index.html.
Thanks in advance.
Phil
Subclass django.contrib.admin.site.AdminSite(). Override the .index() method, and do something like this:
class MyAdminSite(django.contrib.admin.site.AdminSite):
def index(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["app_list"] = get_app_list_in_custom_order()
return super(MyAdminSite, self).index(request, extra_context)
Instantiate an instance of this subclass with my_admin_site = MyAdminSite(), attach your models to it (using the usual my_admin_site.register()), and attach it to the URLconf; that should do it.
(I haven't tried this, I'm basing this on my reading of the AdminSite source.)
If you don't mind to use a subclass of django.contrib.admin.site.AdminSite(), as expected in cases when you need to customize your admin site, I think it's a feasible idea rewriting "index" and "app_index" methods in the derived class. You can do custom ordering using two dictionaries that store the app declararion order in settings.py and the registration order of models.
Then rewrite the code of the original AdminSite().index() and app_index(), adding a custom order fields ('order') in app_list and order by this field despite 'name'. This is the code, excluding app_index(), that is similar to index() function:
class MyAdminSite(AdminSite):
def __init__(self, name='admin', app_name='admin'):
super(MyAdminSite, self).__init__(name, app_name)
# Model's registration ordering. It's not necessary to
# categorize by app.
self._registry_ord = {}
# App ordering determined by declaration
self._app_ord = { 'auth' : 0 }
app_position = 1
for app in settings.INSTALLED_APPS:
self._app_ord[app] = app_position
app_position += 1
def register(self, model_or_iterable, admin_class=None, **options):
super(MyAdminSite, self).register(model_or_iterable, admin_class, **options)
if isinstance(model_or_iterable, ModelBase):
model_or_iterable = [model_or_iterable]
for model in model_or_iterable:
if model in self._registry:
if self._registry_ord:
self._registry_ord[model._meta.object_name] = max(self._registry_ord.values()) + 1
else:
self._registry_ord[model._meta.object_name] = 1
#never_cache
def index(self, request, extra_context=None):
"""
Displays the main admin index page, which lists all of the installed
apps that have been registered in this site.
"""
app_dict = {}
user = request.user
for model, model_admin in self._registry.items():
app_label = model._meta.app_label
has_module_perms = user.has_module_perms(app_label)
if has_module_perms:
perms = model_admin.get_model_perms(request)
# Check whether user has any perm for this module.
# If so, add the module to the model_list.
if True in perms.values():
info = (app_label, model._meta.module_name)
model_dict = {
'name': capfirst(model._meta.verbose_name_plural),
'perms': perms,
'order': self._registry_ord[model._meta.object_name]
}
if perms.get('change', False):
try:
model_dict['admin_url'] = reverse('admin:%s_%s_changelist' % info, current_app=self.name)
except NoReverseMatch:
pass
if perms.get('add', False):
try:
model_dict['add_url'] = reverse('admin:%s_%s_add' % info, current_app=self.name)
except NoReverseMatch:
pass
if app_label in app_dict:
app_dict[app_label]['models'].append(model_dict)
else:
app_dict[app_label] = {
'name': app_label.title(),
'app_url': reverse('admin:app_list', kwargs={'app_label': app_label}, current_app=self.name),
'has_module_perms': has_module_perms,
'models': [model_dict],
'order': self._app_ord[app_label],
}
# Sort the apps alphabetically.
app_list = app_dict.values()
app_list.sort(key=lambda x: x['order'])
# Sort the models alphabetically within each app.
for app in app_list:
app['models'].sort(key=lambda x: x['order'])
context = {
'title': _('Site administration'),
'app_list': app_list,
}
context.update(extra_context or {})
return TemplateResponse(request, [
self.index_template or 'admin/index.html',
], context, current_app=self.name)
If you use custom AdminSite and you want to include Auth models you probably need this, somewhere in your code (I made it in a specific app to extend user information :
from django.contrib.auth.models import User, Group
from myproject import admin
admin.site.register(User)
admin.site.register(Group)
After doing what #AdminKG said copy the index.html file to the root of the admin directory that you need to create inside the templates directory you declared on you setting.py.
if you you have a clear sorting logic for app_list you can implement it in the .index() method of your AdminSite's subclass. Otherwise you will need to hard code the app list on index.html.
To access something in your template just have it in your context, something like that:
def index(self, request, extra_context=None):
context = {
'app1':get_app('myappname'),
'app2': get_app('mysecondappname'),
# ...
}
context.update(extra_context or {})
context_instance = template.RequestContext(request, current_app=self.name)
return render_to_response(self.index_template or 'admin/terminal_index.html', context,
context_instance=context_instance
)
Now apps objects are available to use on your index.htm
Since you are concerned about the order, you can find my solution helpful.
Basically, I created a filter, which moves desired elements of app_list to the beginning.
#register.filter
def put_it_first(value, arg):
'''The filter shifts specified items in app_list to the top,
the syntax is: LIST_TO_PROCESS|put_it_first:"1st_item[;2nd_item...]"
'''
def _cust_sort(x):
try:
return arg.index(x['name'].lower())
except ValueError:
return dist
arg = arg.split(';')
arg = map(unicode.lower, arg)
dist = len(arg) + 1
value.sort(key=_cust_sort)
return value
However, if you need to remove some elements you can use:
#register.filter
def remove_some(value, arg):
'''The filter removes specified items from app_list,
the syntax is: LIST_TO_PROCESS|remove_some:"1st_item[;2nd_item...]"
'''
arg = arg.split(';')
arg = map(unicode.lower, arg)
return [v for v in value if v['name'].lower() not in arg]
Filters can be chained, so you can use both at the same time.
Filtering functions are not written the way which would make them speed demons, but this template is not being rendered too often by definition.
app_list = admin.site.get_app_list(context['request'])
apply any sort on app_list