How to prevent a changelist(model) to be populated - django

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

Related

Filtering a model in a CreateView with get_queryset

I'm trying to filter a model with get_queryset() and it seems to work in the view but not in the template.
My view :
class FolderCreate(CreateView):
fields = ['name', 'parent']
template_name = 'Form/folder_create.html'
def get_queryset(self):
folders = Folder.objects.filter(owner=self.request.user)
print folders # ==> return [<Folder: Folder>, <Folder: Another folder>]
return folders
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.owner = self.request.user
return super(FolderCreate, self).form_valid(form)
def get_initial(self):
if self.request.method == 'GET':
foldersUrl = self.request.META['HTTP_REFERER'].split('/')
foldersUrl.pop()
folder = urllib2.unquote(foldersUrl[-1])
try:
return {'parent' : Folder.objects.get(name=folder, owner=self.request.user)}
except Folder.DoesNotExist:
pass
As you can see, folders return two objects related to the session user in get_queryset() : 'Folder' and 'Another folder
Infortunately, the combobox of my template get all the folders, without any filtering.
Any idea ?
The issue here is that get_queryset is not used in a CreateView, as it's meant for filtering the models returned for display in a list or detail view. You want something completely different: you want to filter the choices available in a form field.
To do that you will need to create a custom ModelForm that accepts a user kwarg and filters the queryset accordingly:
class FolderForm(forms.ModelForm):
class Meta:
model = Folder
fields = ['name', 'parent']
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super(FolderForm, self).__init__(*args, **kwargs)
self.fields['parent'].queryset = Folder.objects.filter(user=user)
and then change your view to use that form and pass in the user parameter:
class FolderCreate(CreateView):
template_name = 'Form/folder_create.html'
form_class = FolderForm
def get_form_kwargs(self):
kwargs = super(FolderCreate, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs

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.

Subclassing Django ListView

I'm using Django v1.4 and I'm trying to subclass the generic ListView view. Here's the code
from django.views.generic import ListView
class SearchListView(ListView):
model = None
fields = None
def get_queryset(self):
#...etc...
return super(SearchListView, self).get_queryset()
Then I'll further customize that view for a particular model:
class PersonSearchListView(SearchListView):
model = Person
fields = ['first_name', 'last_name']
So what happens is, ImproperlyConfigured exceptions are the superclass (ListView) stating that either model or queryset should be defined. I thought I was... (model = Person). Why is this value not making it into the view?
Thanks
When you call super(SearchListView, self).get_queryset()
You will call the get_queryset of the below class, as you can see it will raise an exception if you didn't set the model or queryset.
ListView is a child of MultipleObjectMixin.
But if you instantiate a PersonSearchListView, the model should have been set correctly. Could you include the url config? Will try it out later and update my answer.
class MultipleObjectMixin(ContextMixin):
"""
A mixin for views manipulating multiple objects.
"""
allow_empty = True
queryset = None
model = None
paginate_by = None
context_object_name = None
paginator_class = Paginator
def get_queryset(self):
"""
Get the list of items for this view. This must be an iterable, and may
be a queryset (in which qs-specific behavior will be enabled).
"""
if self.queryset is not None:
queryset = self.queryset
if hasattr(queryset, '_clone'):
queryset = queryset._clone()
elif self.model is not None:
queryset = self.model._default_manager.all()
else:
raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'"
% self.__class__.__name__)
return queryset

Django-admin order by multiple fields

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 :-)

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')