Django data sharding - django

I have successfully got my application running over several databases using the routing scheme based on models. I.e. model A lives on DB A and model B lives on DB B. I now need to shard my data. I am looking at the docs and having trouble working out how to do it as the same model needs to exist on multiple database servers. I want to have a flag to say DB for NEW members is now database X and that members X-Y live on database N etc.
How do I do that? Is it using **hints, this seems inadequately documented to me.

The hints parameter is designed to help your database router decide where it should read or write its data. It may evolve with future versions of python, but for now there's just one kind of hint that may be given by the Django framework, and that's the instance it's working on.
I wrote this very simple database router to see what Django does:
# routers.py
import logging
logger = logging.getLogger("my_project")
class DebugRouter(object):
"""A debugging router"""
def db_for_read(self, model, **hints):
logger.debug("db_for_read %s" % repr((model, hints)))
return None
def db_for_write(self, model, **hints):
logger.debug("db_for_write %s" % repr((model, hints)))
return None
def allow_relation(self, obj1, obj2, **hints):
logger.debug("allow_relation %s" % repr((obj1, obj2, hints)))
return None
def allow_syncdb(self, db, model):
logger.debug("allow_syncdb %s" % repr((db, model)))
return None
You declare this in settings.py:
DATABASE_ROUTERS = ["my_project.routers.DebugRouter"]
Make sure logging is properly configured to output debug output (for example to stderr):
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
[...some other handlers...]
'stderr': {
'level': 'DEBUG',
'class': 'logging.StreamHandler'
}
},
'loggers': {
[...some other loggers...]
'my_project': {
'handlers': ['stderr'],
'level': 'DEBUG',
'propagate': True,
},
}
}
Then you can open a Django shell and test a few requests to see what data your router is being given:
$ ./manage.py shell
[...]
>>> from my_project.my_app.models import User
>>> User.objects.get(pk = 1234)
db_for_read (<class 'my_project.my_app.models.User'>, {})
<User: User object>
>>> user = User.objects.create(name = "Arthur", title = "King")
db_for_write (<class 'my_project.my_app.models.User'>, {})
>>> user.name = "Kong"
>>> user.save()
db_for_write (<class 'my_project.my_app.models.User'>, {'instance':
<User: User object>})
>>>
As you can see, the hints is always empty when no instance is available (in memory) yet. So you cannot use routers if you need query parameters (the object's id for example) in order to determine which database to query. It might be possible in the future if Django provides the query or queryset objects in the hints dict.
So to answer your question, I would say that for now you must create a custom Manager, as suggested by Aaron Merriam. But overriding just the create method is not enough, since you also need to be able to fetch an object in the appropriate database. Something like this might work (not tested yet):
class CustomManager(models.Manager)
def self.find_database_alias(self, pk):
return #... implement the logic to determine the shard from the pk
def self.new_object_database_alias(self):
return #... database alias for a new object
def get(self, *args, **kargs):
pk = kargs.get("pk")
if pk is None:
raise Exception("Sharded table: you must provide the primary key")
db_alias = self.find_database_alias(pk)
qs = self.get_query_set().using(db_alias)
return qs.get(*args, **kargs)
def create(self, *args, **kwargs):
db_alias = self.new_object_database_alias()
qs = super(CustomManager, self).using(db_alias)
return qs.create(*args, **kwargs)
class ModelA(models.Model):
objects = CustomManager()
Cheers

using should allow you to designate which database you want to use.
subclassing the create method might accomplish what you're looking to do.
class CustomManager(models.Manager)
def get_query_set(self):
return super(CustomManager, self).get_query_set()
def create(self, *args, **kwargs):
return super(CustomManager, self).using('OTHER_DB').create(*args, **kwargs)
class ModelA(models.Model):
objects = CustomManager()
I have not tested this so I don't know if you can tack a 'create' onto the end of a 'using'

Related

Django specify which database to use for module

In my Django project I have a couple of applications, one of them is email_lists and this application does a lot of data handling reading data from the Model Customers. In my production environment I have two databases: default and read-replica. I would like all queries in a particular module to be made against the replica-set database.
I can do that if I explicitly tell the query to do so:
def get_customers(self):
if settings.ENV == 'production':
customers = Customer.objects.using('read-replica').filter()
else:
customers = Customer.objects.filter()
but this module has more than 100 queries to the Customer and other models. I also have queries to relations like:
def get_value(self, customer):
target_sessions = customer.sessions.filter(status='open')
carts = Cart.objects.filter(session__in=target_sessions)
the idea is that I want to avoid writing:
if settings.ENV == 'production':
instance = Model.objects.using('read-replica').filter()
else:
instance = Model.objects.filter()
for every query. There are other places in my project that do need to read from default database so it can't be a global setting. I just need this module or file to read using the replica.
Is this possible in Django, are there any shortcuts ?
Thanks
You can read on django database routers for this, some good examples can be found online as well and they should be straightforward.
--
Another solution would be to modify the Model manager.
from django.db import models
class ReplicaRoutingManager(models.Manager):
def get_queryset(self):
queryset = super().get_queryset(self)
if settings.ENV == 'production':
return queryset.using('read-replica')
return queryset
class Customer(models.Model):
...
objects = models.Manager()
replica_objects = ReplicaRoutingManager()
with this, you can just use the normal Customer.objects.filter and the manager should do the routing.
I still suggest going with the database router solution, and creating a custom logic in the class. But if the manager works for you, its fine.
If you want All the queries in the email_lists app to query read-replica, then a router is the way to go. If you need to query different databases within the same app, then #ibaguio's solution is the way to go. Here's a basic router example similar to what I'm using:
project/database_routers.py
MAP = {'some_app': 'default',
'some_other_app': 'default',
'email_lists': 'read-replica',}
class DatabaseRouter:
def db_for_read(self, model, **hints):
return MAP.get(model._meta.app_label, None)
def db_for_write(self, model, **hints):
return MAP.get(model._meta.app_label, None)
def allow_relation(self, object_1, object_2, **hints):
database_object_1 = MAP.get(object_1._meta.app_label)
database_object_2 = MAP.get(object_2._meta.app_label)
return database_object_1 == database_object_2
def allow_migrate(self, db, app_label, model=None, **hints):
return MAP.get(app_label, None)
In settings.py:
DATABASE_ROUTERS = ['project.database_router.DatabaseRouter',]
It looks like you only want it in production, so I would think you could add it conditionally:
if ENV == 'production':
DATABASE_ROUTERS = ['project.database_router.DatabaseRouter',]

Model there should only be one instance of [duplicate]

I would like to control some configuration settings for my project using a database model. For example:
class JuicerBaseSettings(models.Model):
max_rpm = model.IntegerField(default=10)
min_rpm = model.IntegerField(default=0)
There should only be one instance of this model:
juicer_base = JuicerBaseSettings()
juicer_base.save()
Of course, if someone accidentally creates a new instances, it's not the end of the world. I could just do JuicerBaseSettings.objects.all().first(). However, is there a way to lock it down such that it's impossible to create more than 1 instance?
I found two related questions on SO. This answer suggests using 3rd party apps like django-singletons, which doesn't seem to be actively maintained (last update to the git repo is 5 years ago). Another answer suggests using a combination of either permissions or OneToOneField. Both answers are from 2010-2011.
Given that Django has changed a lot since then, are there any standard ways to solve this problem? Or should I just use .first() and accept that there may be duplicates?
You can override save method to control number of instances:
class JuicerBaseSettings(models.Model):
def save(self, *args, **kwargs):
if not self.pk and JuicerBaseSettings.objects.exists():
# if you'll not check for self.pk
# then error will also raised in update of exists model
raise ValidationError('There is can be only one JuicerBaseSettings instance')
return super(JuicerBaseSettings, self).save(*args, **kwargs)
Either you can override save and create a class function JuicerBaseSettings.object()
class JuicerBaseSettings(models.Model):
#classmethod
def object(cls):
return cls._default_manager.all().first() # Since only one item
def save(self, *args, **kwargs):
self.pk = self.id = 1
return super().save(*args, **kwargs)
============= OR =============
Simply, Use django_solo.
https://github.com/lazybird/django-solo
Snippet Courtsy: django-solo-documentation.
# models.py
from django.db import models
from solo.models import SingletonModel
class SiteConfiguration(SingletonModel):
site_name = models.CharField(max_length=255, default='Site Name')
maintenance_mode = models.BooleanField(default=False)
def __unicode__(self):
return u"Site Configuration"
class Meta:
verbose_name = "Site Configuration"
# admin.py
from django.contrib import admin
from solo.admin import SingletonModelAdmin
from config.models import SiteConfiguration
admin.site.register(SiteConfiguration, SingletonModelAdmin)
# There is only one item in the table, you can get it this way:
from .models import SiteConfiguration
config = SiteConfiguration.objects.get()
# get_solo will create the item if it does not already exist
config = SiteConfiguration.get_solo()
If your model is used in django-admin only, you additionally can set dynamic add permission for your model:
# some imports here
from django.contrib import admin
from myapp import models
#admin.register(models.ExampleModel)
class ExampleModelAdmin(admin.ModelAdmin):
# some code...
def has_add_permission(self, request):
# check if generally has add permission
retVal = super().has_add_permission(request)
# set add permission to False, if object already exists
if retVal and models.ExampleModel.objects.exists():
retVal = False
return retVal
i am not an expert but i guess you can overwrite the model's save() method so that it will check if there has already been a instance , if so the save() method will just return , otherwise it will call the super().save()
You could use a pre_save signal
#receiver(pre_save, sender=JuicerBaseSettings)
def check_no_conflicting_juicer(sender, instance, *args, **kwargs):
# If another JuicerBaseSettings object exists a ValidationError will be raised
if JuicerBaseSettings.objects.exclude(pk=instance.pk).exists():
raise ValidationError('A JuiceBaseSettings object already exists')
I'm a bit late to the party but if you want to ensure that only one instance of an object is created, an alternative solution to modifying a models save() function would be to always specify an ID of 1 when creating an instance - that way, if an instance already exists, an integrity error will be raised.
e.g.
JuicerBaseSettings.objects.create(id=1)
instead of:
JuicerBaseSettings.objects.create()
It's not as clean of a solution as modifying the save function but it still does the trick.
I did something like this in my admin so that I won't ever go to original add_new view at all unless there's no object already present:
def add_view(self, request, form_url='', extra_context=None):
obj = MyModel.objects.all().first()
if obj:
return self.change_view(request, object_id=str(obj.id) if obj else None)
else:
return super(type(self), self).add_view(request, form_url, extra_context)
def changelist_view(self, request, extra_context=None):
return self.add_view(request)
Works only when saving from admin

Disabling options in django-autocomplete-light

Just started using django-autocomplete-light (autocomplete.ModelSelect2) and while I have managed to get it working, I wondered if it is possible to pass disabled options?
I have a list of customers to choose from but some, for various reasons, shouldn't be selected they shouldn't be able to use them. I know I could filter these non-selectable customers out, but this wouldn't be very usable as the user might think that the customer isn't in the database. If so, could someone point me in the right direction as I'm not sure where to start.
It says in the Select2 documentation that disabling options should be possible. Presumably if I could also send a 'disabled':true within the returned json response that might do it.
OK, so here is what I came up with and it works.
view.py
The Select2ViewMixin is subclassed and then a 'disabled' attribute is added to the customer details. This value provided by the ParentAutocomplete view.
from dal import autocomplete
from dal_select2.views import Select2ViewMixin
from dal.views import BaseQuerySetView
class CustomSelect2ViewMixin(Select2ViewMixin):
def get_results(self, context):
return [
{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
'disabled': self.is_disabled_choice(result), # <-- this gets added
} for result in context['object_list']
]
class CustomSelect2QuerySetView(CustomSelect2ViewMixin, BaseQuerySetView):
"""Adds ability to pass a disabled property to a choice."""
class ParentAutocomplete(CustomSelect2QuerySetView):
def get_queryset(self):
qs = Customer.objects.all()
if self.q:
qs = qs.filter(org_name__icontains=self.q)
return qs.order_by('org_name', 'org_city')
def get_result_label(self, item):
return item.selector_name
def get_selected_result_label(self, item):
return item.selector_name
def is_disabled_choice(self, item): # <-- this is where we determine if the record is selectable or not.
customer_id = self.forwarded.get('customer_id', None)
return not (item.can_have_children and not str(item.pk) == customer_id)
form.py
The form is then used as normal.
from dal import autocomplete
class CustomerBaseForm(forms.ModelForm):
customer_id= forms.IntegerField(required=False, widget=forms.HiddenInput)
class Meta:
model = Customer
widgets = {
'parent':autocomplete.ModelSelect2(
url='customer:parent-autocomplete',
forward=['customer_id'],
)
}
Hopefully this might be useful to someone.

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

The default "delete selected" admin action in 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)