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
Related
Good afternoon! I'm using the verbose_name_plural dynamic field to show some up-to-date information in the admin panel.
Django version: 4.1
The model looks like this:
from django.utils.functional import lazy
from django.utils.translation import gettext_lazy as _
class Post(models.Model):
# ...
class Meta:
verbose_name = 'Post'
verbose_name_plural = lazy(lambda: _('Posts ({})').format(Post.....count()), str)()
I don't remember where I found this option to display some information, but it works great, except that every time the value changes and the command to create migrations is run, I get something like this:
from django.db import migrations
class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='post',
options={'verbose_name': 'Post', 'verbose_name_plural': 'Posts (123)'},
),
]
I found this option: https://stackoverflow.com/a/39801321/2166371
But I don’t know how relevant it is and why a class that is not used is imported there
Based on the variant that 0sVoid suggested, I implemented a slightly different one that seems to work without problems.
Initially, it was proposed to change one method in the main class of the admin panel (_build_app_dict): https://stackoverflow.com/a/71740645/2166371
But there is too much code in this method that I didn't want to rewrite
I rewrote another method get_app_list
Parent method:
def get_app_list(self, request, app_label=None):
"""
Return a sorted list of all the installed apps that have been
registered in this site.
"""
app_dict = self._build_app_dict(request, app_label)
# Sort the apps alphabetically.
app_list = sorted(app_dict.values(), key=lambda x: x["name"].lower())
# Sort the models alphabetically within each app.
for app in app_list:
app["models"].sort(key=lambda x: x["name"])
return app_list
My version:
def get_app_list(self, request, app_label=None):
"""
Return a sorted list of all the installed apps that have been
registered in this site.
"""
app_dict = self._build_app_dict(request, app_label)
# Sort the apps alphabetically.
app_list = sorted(app_dict.values(), key=lambda x: x["name"].lower())
# Sort the models alphabetically within each app.
for app in app_list:
app["models"].sort(key=lambda x: x["name"])
for model in app['models']:
if hasattr(model['model'], 'get_name_admin_panel'):
model['name'] = model['model'].get_name_admin_panel()
return app_list
I have added 3 new lines, before return (these lines can also be added before sorting if sequence is important to you)
There is a check that if the get_name_admin_panel method is described for the model, then we change its name to the one that the method returns
In the model itself, I added the following lines:
#classmethod
def get_name_admin_panel(cls):
return _('Posts ({})').format(Post.objects.filter(...).count())
Thanks to this, I can now set a static value for the verbose_name_plural field and the migration will not be repeated many times
If you do not specify a #classmethod, then unfortunately the admin panel page will load with an error, but I didn't see any problem with that.
I have bunch of function based views that have similar functionality
For example create, list, edit view and search companies and contacts of: Customers , Vendors and Manufactures.
I can reuse the same model with few boolean flags for all 3 of them.
But I was wondering how to deal with view and templates in a best way to avoid code duplications. (currently I am thinking but massive usage of control-H but it doesn't feel right)
(I regret now not using the Class based views but it is too late)
There is a lot you can do by passing in url parameters to achieve reuse. Your imagination and consistency will help a lot here:
For a simple example:
urlpatterns = [
url(r'^add-customer/$', create, {'template':'customer.html'}, name='create_customer'),
url(r'^add-vendor/$', create, {'template':'vendor.html'}, name='create_vendor'),
url(r'^add-manufacturer/$', create, {'template':'manufacturer.html'}, name='create_manufacturer'),
...
and the view:
def create(request, template):
#use template as you like, coming from url conf.
return render(request, template)
...
That's probably normal code when you think about it. A little more interesting example:
from someapp.forms import *
from someapp.models import *
urlpatterns = [
url(r'^customer/$', create, {'template':'customer.html', 'form_class':customer_form}, name='create_customer'),
url(r'^vendor/$', create, {'template':'vendor.html', 'form_class':vendor_form}, name='create_vendor'),
url(r'^manufacturer/$', create, {'template':'manufacturer.html', 'form_class':manufacturer_form}, name='create_manufacturer'),
...
and the view:
def create(request, template, form_class):
#use template as you like, coming from url conf.
form = form_class()#
return render(request, template)
...
Still pretty much normal code. You can get funky and generate the urls dynamically:
In your urls.py:
for form_class in (CustomerForm, VendorForm, ManufacturerForm): #model forms
model = form_class._meta.model
model_name = model._meta.module_name
urlpatterns += url(r'^create/$', create, {'template':'%s.html' % model_name, 'form_class':form_class, 'model':model}, name='create_%s' % model_name),
and in the view:
def create(request, template, form_class, model):
#use template as you like, coming from url conf.
form = form_class()#
return render(request, template)
You can pass in pks to models and write codes like:
def create(request, form_class, model, id=None):
instance = get_object_or_404(model, pk=id) if id else None
edited = True if instance else False
if request.method == 'POST':
form = form_class(data=request.POST, files=request.FILES, instance=instance)
if form.is_valid():
instance = form.save()
...
else:
form = form_class(instance=instance)
return render(request, template_name, {'form': form, 'instance': instance})
Hopefully you have the stomach for it.
The answer, of course, depends on what actually is different between the models. But if you can abstract your view functions "with a few boolean flags" I would suggest to put them in a central place, probably a common file in your app or project, e.g. abstract_views.py.
Then you will only need to dispatch the view functions in a meaningful way:
import my_app.abstract_views
from .models import Model1, Model2
def model1_create_view(request):
abstract_views.create(request, model=Model1)
def model2_create_view(request):
abstract_views.create(request, model=Model2)
In this hypthetical example create would be the abstract view function for creating an instance and it would require a parameter model with the actual model to operate on.
ADDED:
Alternatively, use boolean flags, as requested:
import my_app.abstract_views
def model1_create_view(request):
abstract_views.create(request, is_customer=True)
def model2_create_view(request):
abstract_views.create(request, is_vendor=True)
When using boolean flags, remember to define what happens if someone is both, a customer and a vendor...
The definition of the abstract view then reads something like:
def abstract_view(request, is_customer=False, is_vendor=False):
context = do_something (is_customer, is_vendor)
return(render(request, 'app/template.html', context))
Hope that helps.
I need to get a changelist view queryset in django admin. Currently, I have this monkey patch which makes 4 extra queries, so I'm looking for a better solution.
My point is: I want to pass some extra values to django admin change_list.html template which I get from creating queries. For those queries, I need the queryset which is used in django admin changelist view with request filters applied. This is the same data which I see filtered, ordered etc. I want to make graphs from this data.
Do you understand me? Thanks
#admin.py
from django.contrib.admin.views.main import ChangeList
class TicketAdmin(admin.ModelAdmin):
def changelist_view(self, request, extra_context=None):
cl = ChangeList(request,
self.model,
self.list_display,
self.list_display_links,
self.list_filter,
self.date_hierarchy,
self.search_fields,
self.list_select_related,
self.list_per_page,
self.list_max_show_all,
self.list_editable,
self) # 3 extra queries
filtered_query_set = cl.get_query_set(request) # 1 extra query
currencies_count = filtered_query_set.values('bookmaker__currency').distinct().count()
extra_context = {
'currencies_count': currencies_count,
}
return super(TicketAdmin, self).changelist_view(request, extra_context=extra_context)
I don't know if this answers to your question but the class ChangeList has an attribute called query_set (you can find the code here https://github.com/django/django/blob/master/django/contrib/admin/views/main.py) already containing the queryset.
BTW the changelist_view() function (source at https://github.com/django/django/blob/master/django/contrib/admin/options.py) returns a TemplateResponse (source at https://github.com/django/django/blob/master/django/template/response.py) that has a variable named context_data which points to the context. You can try to extend the content of this variable.
Below follows the untested code
class TicketAdmin(admin.ModelAdmin):
def changelist_view(self, request, extra_context=None):
response = super(TicketAdmin, self).changelist_view(request, extra_context)
filtered_query_set = response.context_data["cl"].queryset
currencies_count = filtered_query_set.values('bookmaker__currency').distinct().count()
extra_context = {
'currencies_count': currencies_count,
}
response.context_data.update(extra_context)
return response
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)
In django admin I wanted to set up a custom filter by tags (tags are introduced with django-tagging)
I've made the ModelAdmin for this and it used to work fine, by appending custom urlconf and modifying the changelist view. It should work with URLs like: http://127.0.0.1:8000/admin/reviews/review/only-tagged-vista/
But now I get 'invalid literal for int() with base 10: 'only-tagged-vista', error which means it keeps matching the review edit page instead of the custom filter page, and I cannot figure out why since it used to work and I can't find what change might have affected this.
Any help appreciated.
Relevant code:
class ReviewAdmin(VersionAdmin):
def changelist_view(self, request, extra_context=None, **kwargs):
from django.contrib.admin.views.main import ChangeList
cl = ChangeList(request, self.model, list(self.list_display),
self.list_display_links, self.list_filter,
self.date_hierarchy, self.search_fields,
self.list_select_related,
self.list_per_page,
self.list_editable,
self)
cl.formset = None
if extra_context is None:
extra_context = {}
if kwargs.get('only_tagged'):
tag = kwargs.get('tag')
cl.result_list = cl.result_list.filter(tags__icontains=tag)
extra_context['extra_filter'] = "Only tagged %s" % tag
extra_context['cl'] = cl
return super(ReviewAdmin, self).changelist_view(request, extra_context=extra_context)
def get_urls(self):
from django.conf.urls.defaults import patterns, url
urls = super(ReviewAdmin, self).get_urls()
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
return update_wrapper(wrapper, view)
info = self.model._meta.app_label, self.model._meta.module_name
my_urls = patterns('',
# make edit work from tagged filter list view
# redirect to normal edit view
url(r'^only-tagged-\w+/(?P<id>.+)/$',
redirect_to,
{'url': "/admin/"+self.model._meta.app_label+"/"+self.model._meta.module_name+"/%(id)s"}
),
# tagged filter list view
url(r'^only-tagged-(P<tag>\w+)/$',
self.admin_site.admin_view(self.changelist_view),
{'only_tagged':True}, name="changelist_view"),
)
return my_urls + urls
Edit: Original issue fixed.
I now receive 'Cannot filter a query once a slice has been taken.' for line:
cl.result_list = cl.result_list.filter(tags__icontains=tag)
I'm not sure where this result list is sliced, before tag filter is applied.
Edit2:
It's because of the self.list_per_page in ChangeList declaration. However didn't find a proper solution yet. Temp fix:
if kwargs.get('only_tagged'):
list_per_page = 1000000
else:
list_per_page = self.list_per_page
cl = ChangeList(request, self.model, list(self.list_display),
self.list_display_links, self.list_filter,
self.date_hierarchy, self.search_fields,
self.list_select_related,
list_per_page,
self.list_editable,
self)
You're missing a question mark in before the P in '^only-tagged-(P<tag>\w+)/$', so the expression won't match.
In code sample above, get_urls() is aligned so that it is not part of ReviewAdmin class but rather a separate function. I guess that can cause your problem if you have it the same way in real source.
The error apears on multi-word tags because you match just one word tags.
this works: r'^only-tagged-(?P[^/]+)/$'