Django-admin order by multiple fields - django

How do I order by multiple fields in the django-admin?
Thanks

Try this:
Set ordering in your model Meta:
class Meta:
ordering = ["some_field", "other_field"]
and add this class in admin.py:
from django.contrib.admin.views.main import ChangeList
class SpecialOrderingChangeList(ChangeList):
"""
Django 1.3 ordering problem workaround
from 1.4 it's enough to use `ordering` variable
"""
def get_query_set(self):
queryset = super(SpecialOrderingChangeList, self).get_query_set()
return queryset.order_by(*self.model._meta.ordering)
Add this method in your admin.ModelAdmin
def get_changelist(self, request, **kwargs):
return SpecialOrderingChangeList
source: https://groups.google.com/forum/?fromgroups#!topic/django-users/PvjClVVgD-s

until django 1.4 (currently in alpha) the django admin only orders by the first column in Meta ordering. You can work around this by overriding the queryset:
class MyAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(MyAdmin, self).queryset(request)
qs = qs.order_by('last_name', 'first_name')
return qs

Further to user535010's response above:
I struggled because after adding the suggested code I was no longer able to order the fields by clicking on the headings in the admin list view. I modified the get_changelist method suggested for MyModelAdmin as follows:
def get_changelist(self, request, **kwargs): #ordering issue in 1.3 workaround
try:
if not request.GET['o']:
return SpecialOrderingChangeList
except KeyError:
pass
return super(MyModelAdmin, self).get_changelist(request)

Django model admin supports ordering by multiple values in Django 2.0+. You can now use it like this:
class MyAdmin(admin.ModelAdmin):
ordering = ['last_name', 'first_name']

The function required to make click-ordering work with the multi-column sort fix is this:
def get_changelist(self, request, **kwargs):
try:
if request.GET['o']:
return super(ModelAdmin, self).get_changelist(request)
except KeyError:
pass
return SpecialOrderingChangeList
Other way round to jenniwren's answer :-)

Related

Creating a generic search view in Django

I am struggling to create my custom generic view in django to easily create search pages for certain models. I'd like to use it like this:
class MyModelSearchView(SearchView):
template_name = 'path/to/template.html'
model = MyModel
fields = ['name', 'email', 'whatever']
which will result in a view that returns a search form on GET and both form and results on POST.
The fields specifies which fields of MyModel will be available for a user to search.
class SearchView(FormView):
def get_form(self, form_class=None):
# what I'v already tried:
class SearchForm(ModelForm):
class Meta:
model = self.model
fields = self.fields
return SearchForm()
def post(self, request, *args, **kwargs):
# perform searching and return results
The problem with the code above is that form will not be submitted if certain fields are not be properly filled. User should be allowed to provide only part of fields to search but with the code I provided the form generated with ModelForm prevents that (for example because a field in a model cannot be blank).
My questions are:
Is it possible to generate a form based on a model to omit this behaviour?
Or is there any simpler way to create SearchView class?
I don't want to manually write forms if it's possible.
One way to accomplish this is to set blank=True on the field in MyModel, as indicated in the docs:
If the model field has blank=True, then required is set to False on the form field. Otherwise, required=True.
But for this to be a generic solution, you can't count on being able to modify the model fields. You can instead set the fields' required attribute to False immediately after the instance is created:
class SearchForm(ModelForm):
class Meta:
model = self.model
fields = self.fields
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for (field_name, field) in self.fields.items():
field.required = False
Since you're using the ModelForm for searching, you should set all the fields as required=False, by overriding the __init__ method:
def get_form(self, form_class=None):
# what I'v already tried:
class SearchForm(ModelForm):
class Meta:
model = self.model
fields = self.fields
def __init__(self, *args, **kwargs):
super(SearchForm, self).__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].required = False
return SearchForm()
Though I suggest you should user django-filter, which makes it easier and cleaner to filter your searches. First you need to install it:
pip install django-filter
Then add it to your INSTALLED_APPS. After that you can create a filters.py file in your app:
# myapp/filters.py
import django_filters as filters
from .models import MyModel
MyModelFilterSet(filters.FilterSet):
class Meta:
model = MyModel
fields = ['name', 'email', 'whatever']
By default it's going to filter with the __exact lookup. You can change this in a couple of ways, just take a look here and here. To know which lookups you can use, take a look here.
After creating your filters.py file you can add it to a View, like a ListView:
# myapp/views.py
from django.views.generic import ListView
from .filters import MyModelFilterSet
from .models import MyModel
class MyModelSearchView(ListView):
template_name = 'path/to/template.html'
model = MyModel
def get_queryset(self):
qs = self.model.objects.all()
filtered_model_list = MyModelFilterSet(self.request.GET, queryset=qs)
return filtered_model_list.qs
There's a lot more you can do with django-filter. Here's the full documentation.

How to prevent a changelist(model) to be populated

I'm new to Django and for a small project I need to open changelist of some models without any data. How we can prevent initial populating of some models?
You have to create your own ChangeList subclass. For example the following code will open an empty changelist but if you will search for name then the list will be populated by the search results:
from django.contrib.admin.views.main import ChangeList
class MyChangeList(ChangeList):
def get_queryset(self, request):
queryset = super(MyChangeList, self).get_queryset(request)
if not request.GET.get('q', ''):
queryset = queryset.none()
return queryset
class MyAdmin(admin.ModelAdmin):
search_fields = ['name']
def get_changelist1(self, request, **kwargs):
return MyChangeList

Get the current queryset shown in ChangeList from the ModelAdmin class

I need to work in ModelAdmin with the elements shown in the ChangeList, but I don't know how to get the current queryset.
For example, if now the first 100 elements are being shown, I want to work with this set, and if the user pass to the next 100, I want to have the new 100 elements in the set.
Other example is when some user applies some filter.
In my Model Admin I have:
list_display = getListDisplay(qs)
And I want to pass to getListDisplay the current queryset, because depending on it, the list_display will be different.
Is there any current queryset attribute somewhere accessible from ModelAdmin class?
After UPD 1 I cannot make this works:
class YourAdmin(admin.ModelAdmin):
def get_queryset(self):
qs = super(YourAdmin, self).get_queryset()
return qs
def __init__(self, *args, **kwargs):
super(YourAdmin, self).__init__(*args, **kwargs)
qs = self.get_queryset()
Here's a link to the documentation for ModelAdmin.get_queryset()
Edit in response to your comments:
It seems to me that what you want to do is build the list_display dynamically. Django has a get_list_display method for model admin. This method receives the request which you can then pass to get_queryset:
class YourAdmin(admin.ModelAdmin):
def get_list_display(self, request):
qs = self.get_queryset(request)
'''
Now build the list_display as a list or tuple
'''
.
.
return list_display
In django admin, there is a thing called actions. It is described in here and works like this:
class YourAdmin(admin.ModelAdmin):
def make_something(self, request, queryset):
queryset.update(status='p')
actions = ['make_something',]
Maybe queryset is the thing you need
UPD 1: After comment, I understood that you need to modify some attributes of your ModelAdmin ojbect. So you can do that right after initialization:
class YourAdmin(admin.ModelAdmin):
def __init__(self, *args, **kwargs):
super(YourAdmin, self).__init__(*args, **kwargs)
qs = self.getquery_set()
// modify your list_display depending on qs
self.list_display = ['some','fileds','here']
Some third-party apps like xadmin allow you to change list_display on the fly, but django doesn't have this feature by default.

unique_together and implicitly filled-in field in Django admin

Say I'm writing a multi-blog application and I want each author to use unique titles for their articles (but unique per user, not globally unique):
class Article(models.Model):
author = models.ForeignKey('auth.User')
title = models.CharField(max_length=255)
#[...]
class Meta:
unique_together = (('title', 'owner'),)
Now, I want the author field to be auto-filled by the application:
class ArticleAdmin(ModelAdmin):
exclude = ('owner',)
def save_model(self, request, obj, form, change):
if not change:
obj.owner = request.user
obj.save()
Actually this does not work: if I try to create a new Article with an existing author-title combination, Django will not check the uniqueness (because author is excluded from the form) and I'll get an IntegrityError when it hits the database.
I thought of adding a clean method to the Article class:
def clean(self):
if Article.objects.filter(title=self.title, owner=self.owner).exists():
raise ValidationError(u"...")
But it seems that Article.clean() is called before ArticleAdmin.save_model(), so this does not work.
Several variants of this question have been asked already here, but none of the solutions seem to work for me:
I cannot use Form.clean() or other form methods that don't have the request available, since I need the request.user.
For the same reason, model-level validation is not possible.
Some answers refer to class-based views or custom views, but I'd like to remain in the context of Django's Admin.
Any ideas how I can do this without rewriting half of the admin app?
You are finding a way to bring request to customized form, in ModelAdmin, actually:
from django.core.exceptions import ValidationError
def make_add_form(request, base_form):
class ArticleForm(base_form):
def clean(self):
if Article.objects.filter(title=self.cleaned_data['title'], owner=request.user).exists():
raise ValidationError(u"...")
return self.cleaned_data
def save(self, commit=False):
self.instance.owner = request.user
return super(ArticleForm, self).save(commit=commit)
return ArticleForm
class ArticleAdmin(admin.ModelAdmin):
exclude = ('owner',)
def get_form(self, request, obj=None, **kwargs):
if obj is None: # add
kwargs['form'] = make_add_form(request, self.form)
return super(ArticleAdmin, self).get_form(request, obj, **kwargs)

Caching queryset choices for ModelChoiceField or ModelMultipleChoiceField in a Django form

When using ModelChoiceField or ModelMultipleChoiceField in a Django form, is there a way to pass in a cached set of choices? Currently, if I specify the choices via the queryset parameter, it results in a database hit.
I'd like to cache these choices using memcached and prevent unnecessary hits to the database when displaying a form with such a field.
The reason that ModelChoiceField in particular creates a hit when generating choices - regardless of whether the QuerySet has been populated previously - lies in this line
for obj in self.queryset.all():
in django.forms.models.ModelChoiceIterator. As the Django documentation on caching of QuerySets highlights,
callable attributes cause DB lookups every time.
So I'd prefer to just use
for obj in self.queryset:
even though I'm not 100% sure about all implications of this (I do know I do not have big plans with the queryset afterwards, so I think I'm fine without the copy .all() creates). I'm tempted to change this in the source code, but since I'm going to forget about it at the next install (and it's bad style to begin with) I ended up writing my custom ModelChoiceField:
class MyModelChoiceIterator(forms.models.ModelChoiceIterator):
"""note that only line with # *** in it is actually changed"""
def __init__(self, field):
forms.models.ModelChoiceIterator.__init__(self, field)
def __iter__(self):
if self.field.empty_label is not None:
yield (u"", self.field.empty_label)
if self.field.cache_choices:
if self.field.choice_cache is None:
self.field.choice_cache = [
self.choice(obj) for obj in self.queryset.all()
]
for choice in self.field.choice_cache:
yield choice
else:
for obj in self.queryset: # ***
yield self.choice(obj)
class MyModelChoiceField(forms.ModelChoiceField):
"""only purpose of this class is to call another ModelChoiceIterator"""
def __init__(*args, **kwargs):
forms.ModelChoiceField.__init__(*args, **kwargs)
def _get_choices(self):
if hasattr(self, '_choices'):
return self._choices
return MyModelChoiceIterator(self)
choices = property(_get_choices, forms.ModelChoiceField._set_choices)
This does not solve the general problem of database caching, but since you're asking about ModelChoiceField in particular and that's exactly what got me thinking about that caching in the first place, thought this might help.
You can override "all" method in QuerySet
something like
from django.db import models
class AllMethodCachingQueryset(models.query.QuerySet):
def all(self, get_from_cache=True):
if get_from_cache:
return self
else:
return self._clone()
class AllMethodCachingManager(models.Manager):
def get_query_set(self):
return AllMethodCachingQueryset(self.model, using=self._db)
class YourModel(models.Model):
foo = models.ForeignKey(AnotherModel)
cache_all_method = AllMethodCachingManager()
And then change queryset of field before form using (for exmple when you use formsets)
form_class.base_fields['foo'].queryset = YourModel.cache_all_method.all()
Here is a little hack I use with Django 1.10 to cache a queryset in a formset:
qs = my_queryset
# cache the queryset results
cache = [p for p in qs]
# build an iterable class to override the queryset's all() method
class CacheQuerysetAll(object):
def __iter__(self):
return iter(cache)
def _prefetch_related_lookups(self):
return False
qs.all = CacheQuerysetAll
# update the forms field in the formset
for form in formset.forms:
form.fields['my_field'].queryset = qs
I also stumbled over this problem while using an InlineFormset in the Django Admin that itself referenced two other Models. A lot of unnecessary queries are generated because, as Nicolas87 explained, ModelChoiceIterator fetches the queryset everytime from scratch.
The following Mixin can be added to admin.ModelAdmin, admin.TabularInline or admin.StackedInline to reduce the number of queries to just the ones needed to fill the cache. The cache is tied to the Request object, so it invalidates on a new request.
class ForeignKeyCacheMixin(object):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super(ForeignKeyCacheMixin, self).formfield_for_foreignkey(db_field, **kwargs)
cache = getattr(request, 'db_field_cache', {})
if cache.get(db_field.name):
formfield.choices = cache[db_field.name]
else:
formfield.choices.field.cache_choices = True
formfield.choices.field.choice_cache = [
formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
]
request.db_field_cache = cache
request.db_field_cache[db_field.name] = formfield.choices
return formfield
#jnns I noticed that in your code the queryset is evaluated twice (at least in my Admin inline context), which seems to be an overhead of django admin anyway, even without this mixin (plus one time per inline when you don't have this mixing).
In the case of this mixin, this is due to the fact that formfield.choices has a setter that (to simplify) triggers the re-evaluation of the object's queryset.all()
I propose an improvement which consists of dealing directly with formfield.cache_choices and formfield.choice_cache
Here it is:
class ForeignKeyCacheMixin(object):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super(ForeignKeyCacheMixin, self).formfield_for_foreignkey(db_field, **kwargs)
cache = getattr(request, 'db_field_cache', {})
formfield.cache_choices = True
if db_field.name in cache:
formfield.choice_cache = cache[db_field.name]
else:
formfield.choice_cache = [
formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
]
request.db_field_cache = cache
request.db_field_cache[db_field.name] = formfield.choices
return formfield
Here is another solution for preventing ModelMultipleChoiceField from re-fetching it's queryset from database. This is helpful when you have multiple instances of the same form and do not want each form to re-fetch the same queryset. In addition the queryset is a parameter of the form initialization, allowing you e.g. to define it in your view.
Note that the code of those classes have changed in the meantime. This solution uses the versions from Django 3.1.
This example uses a many-2-many relation with Django's Group
models.py
from django.contrib.auth.models import Group
from django.db import models
class Example(models.Model):
name = models.CharField(max_length=100, default="")
groups = models.ManyToManyField(Group)
...
forms.py
from django.contrib.auth.models import Group
from django import forms
class MyModelChoiceIterator(forms.models.ModelChoiceIterator):
"""Variant of Django's ModelChoiceIterator to prevent it from always re-fetching the
given queryset from database.
"""
def __iter__(self):
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
queryset = self.queryset
for obj in queryset:
yield self.choice(obj)
class MyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""Variant of Django's ModelMultipleChoiceField to prevent it from always
re-fetching the given queryset from database.
"""
iterator = MyModelChoiceIterator
def _get_queryset(self):
return self._queryset
def _set_queryset(self, queryset):
self._queryset = queryset
self.widget.choices = self.choices
queryset = property(_get_queryset, _set_queryset)
class ExampleForm(ModelForm):
name = forms.CharField(required=True, label="Name", max_length=100)
groups = MyModelMultipleChoiceField(required=False, queryset=Group.objects.none())
def __init__(self, *args, **kwargs):
groups_queryset = kwargs.pop("groups_queryset", None)
super().__init__(*args, **kwargs)
if groups_queryset:
self.fields["groups"].queryset = groups_queryset
class Meta:
model = Example
fields = ["name", "groups"]
views.py
from django.contrib.auth.models import Group
from .forms import ExampleForm
def my_view(request):
...
groups_queryset = Group.objects.order_by("name")
form_1 = ExampleForm(groups_queryset=groups_queryset)
form_2 = ExampleForm(groups_queryset=groups_queryset)
form_3 = ExampleForm(groups_queryset=groups_queryset)
```
#lai With Django 2.1.2 I had to change the code in the first if-statement from formfield.choice_cache = cache[db_field.name] to formfield.choices = cache[db_field.name] as in the answer from jnns. In the Django version 2.1.2 if you inherit from admin.TabularInline you can override the method formfield_for_foreignkey(self, db_field, request, **kwargs) directly without the mixin. So the code could look like this:
class MyInline(admin.TabularInline):
model = MyModel
formset = MyModelInlineFormset
extra = 3
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
cache = getattr(request, 'db_field_cache', {})
formfield.cache_choices = True
if db_field.name in cache:
formfield.choices = cache[db_field.name]
else:
formfield.choice_cache = [
formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
]
request.db_field_cache = cache
request.db_field_cache[db_field.name] = formfield.choices
return formfield
In my case I also had to override get_queryset to get the benefit from select_related like this:
class MyInline(admin.TabularInline):
model = MyModel
formset = MyModelInlineFormset
extra = 3
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
cache = getattr(request, 'db_field_cache', {})
formfield.cache_choices = True
if db_field.name in cache:
formfield.choices = cache[db_field.name]
else:
formfield.choice_cache = [
formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
]
request.db_field_cache = cache
request.db_field_cache[db_field.name] = formfield.choices
return formfield
def get_queryset(self, request):
return super().get_queryset(request).select_related('my_field')