django admin how to limit selectbox values - django

model:
class Store(models.Model):
name = models.CharField(max_length = 20)
class Admin:
pass
def __unicode__(self):
return self.name
class Stock(Store):
products = models.ManyToManyField(Product)
class Admin:
pass
def __unicode__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length = 128, unique = True)
parent = models.ForeignKey('self', null = True, blank = True, related_name='children')
(...)
def __unicode__(self):
return self.name
mptt.register(Product, order_insertion_by = ['name'])
admin.py:
from bar.drinkstore.models import Store, Stock
from django.contrib import admin
admin.site.register(Store)
admin.site.register(Stock)
Now when I look at admin site I can select any product from the list. But I'd like to have a limited choice - only leaves. In mptt class there's function:
is_leaf_node() -- returns True if
the model instance is a leaf node (it
has no children), False otherwise.
But I have no idea how to connect it
I'm trying to make a subclass: in admin.py:
from bar.drinkstore.models import Store, Stock
from django.contrib import admin
admin.site.register(Store)
class StockAdmin(admin.ModelAdmin):
def queryset(self, request):
return super(StockAdmin, self).queryset(request).filter(ihavenoideawhatfilter)
admin.site.register(Stock, StockAdmin)
but I'm not sure if it's right way, and what filter set.
UPD: This is definetely wrong way. the queryset in class StockAdmin produces list of stocks. But I need to filter product list "on stock" - still don't know how.

Edit: Completely updated this
So the queryset, is finally ok but you need to filter the products on the Stock page select box (I guess?). You can define a custom form for the Stock ModelAdmin.
class StockForm(ModelForm):
products = forms.ModelChoiceField(queryset=Products.objects.filter(lft=F('rght')-1))
class Meta:
model = Stock
class StockAdmin(admin.ModelAdmin):
form = StockForm

Botondus has the right idea, but you can't do that with annotate - that's for aggregations across related querysets. Try using extra instead:
qs = super(StockAdmin, self).queryset(request).extra(
select={ 'desc_count': '(rght-lft-1)/2' }
).filter(desc_count=0)

So far your idea is right, I'm no expert on how to filter this correctly, but if you look at mptt.models.get_descendant_count you see how the number of descendents i calculated, the leaves are those where the count is zero. I guess you will have to make this condition into raw sql!
EDIT: I just read the title of your question again right now, is it now about seectbox values or changing the queryset for the change list?

Related

Reload choices dynamically when using MultipleChoiceFilter

I am trying to construct a MultipleChoiceFilter where the choices are the set of possible dates that exist on a related model (DatedResource).
Here is what I am working with so far...
resource_date = filters.MultipleChoiceFilter(
field_name='dated_resource__date',
choices=[
(d, d.strftime('%Y-%m-%d')) for d in
sorted(resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())
],
label="Resource Date"
)
When this is displayed in a html view...
This works fine at first, however if I create new DatedResource objects with new distinct date values I need to re-launch my webserver in order for them to get picked up as a valid choice in this filter. I believe this is because the choices list is evaluated once when the webserver starts up, not every time my page loads.
Is there any way to get around this? Maybe through some creative use of a ModelMultipleChoiceFilter?
Thanks!
Edit:
I tried some simple ModelMultipleChoice usage, but hitting some issues.
resource_date = filters.ModelMultipleChoiceFilter(
field_name='dated_resource__date',
queryset=resource_models.DatedResource.objects.all().values_list('date', flat=True).order_by('date').distinct(),
label="Resource Date"
)
The HTML form is showing up just fine, however the choices are not accepted values to the filter. I get "2019-04-03" is not a valid value. validation errors, I am assuming because this filter is expecting datetime.date objects. I thought about using the coerce parameter, however those are not accepted in ModelMultipleChoice filters.
Per dirkgroten's comment, I tried to use what was suggested in the linked question. This ends up being something like
resource_date = filters.ModelMultipleChoiceFilter(
field_name='dated_resource__date',
to_field_name='date',
queryset=resource_models.DatedResource.objects.all(),
label="Resource Date"
)
This also isnt what I want, as the HTML now form is now a) displaying the str representation of each DatedResource, instead of the DatedResource.date field and b) they are not unique (ex if I have two DatedResource objects with the same date, both of their str representations appear in the list. This also isnt sustainable because I have 200k+ DatedResources, and the page hangs when attempting to load them all (as compared to the values_list filter, which is able to pull all distinct dates out in seconds.
One of the easy solutions will be overriding the __init__() method of the filterset class.
from django_filters import filters, filterset
class FooFilter(filterset.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
self.filters['user'].extra['choices'] = [(d, d.strftime('%Y-%m-%d')) for d in sorted(
resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())]
except (KeyError, AttributeError):
pass
resource_date = filters.MultipleChoiceFilter(field_name='dated_resource__date', choices=[], label="Resource Date")
NOTE: provide choices=[] in your field definition of filterset class
Results
I tested and verified this solution with following dependencies
1. Python 3.6
2. Django 2.1
3. DRF 3.8.2
4. django-filter 2.0.0
I used following code to reproduce the behaviour
# models.py
from django.db import models
class Musician(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return f'{self.name}'
class Album(models.Model):
artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
release_date = models.DateField()
def __str__(self):
return f'{self.name} : {self.artist}'
# serializers.py
from rest_framework import serializers
class AlbumSerializer(serializers.ModelSerializer):
artist = serializers.StringRelatedField()
class Meta:
fields = '__all__'
model = Album
# filters.py
from django_filters import rest_framework as filters
class AlbumFilter(filters.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters['release_date'].extra['choices'] = self.get_album_filter_choices()
def get_album_filter_choices(self):
release_date_list = Album.objects.values_list('release_date', flat=True).distinct()
return [(date, date) for date in release_date_list]
release_date = filters.MultipleChoiceFilter(choices=[])
class Meta:
model = Album
fields = ('release_date',)
# views.py
from rest_framework.viewsets import ModelViewSet
from django_filters import rest_framework as filters
class AlbumViewset(ModelViewSet):
serializer_class = AlbumSerializer
queryset = Album.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_class = AlbumFilter
Here I've used the django-filter with DRF.
Now, I populated some data through Django Admin console. After that, the album api become as below,
and I got the release_date as
Then, I added new entry through Django admin -- (Screenshot) and I refresh the DRF API endpoint and the possible choices became as below,
I have looked into your problem and I have following suggestions
The Problem
You have got the problem right. Choices for your MultipleChoiceFilter are calculated statically whenever you run server.Thats why they don't get updated dynamically whenever you insert new instance in DatedResource.
To get it working correctly, you have to provide choices dynamically to MultipleChoiceFilter. I searched in documentation but did not find anything regarding this. So here is my solution.
The solution
You have to extend MultipleChoiceFilter and create your own filter class. I have created this and here it is.
from typing import Callable
from django_filters.conf import settings
import django_filters
class LazyMultipleChoiceFilter(django_filters.MultipleChoiceFilter):
def get_field_choices(self):
choices = self.extra.get('choices', [])
if isinstance(choices, Callable):
choices = choices()
return choices
#property
def field(self):
if not hasattr(self, '_field'):
field_kwargs = self.extra.copy()
if settings.DISABLE_HELP_TEXT:
field_kwargs.pop('help_text', None)
field_kwargs.update(choices=self.get_field_choices())
self._field = self.field_class(label=self.label, **field_kwargs)
return self._field
Now you can use this class as replacement and pass choices as lambda function like this.
resource_date = LazyMultipleChoiceFilter(
field_name='dated_resource__date',
choices=lambda: [
(d, d.strftime('%Y-%m-%d')) for d in
sorted(resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())
],
label="Resource Date"
)
Whenever instance of filter will be created choices will be updated dynamically. You can also pass choices statically (without lambda function) to this field if want default behavior.

Django's prefetch_related and select_related on more complex relationships in Admin

I have a somewhat complex relationship between multiple models. A simplified example:
class Country(models.Model):
name = models.CharField([...])
[...]
def __ str__(self):
return f'{self.name}'
class Region(models.Model):
country = models.ForeignKey(Country)
name = models.CharField([...])
[...]
def __ str__(self):
return f'{self.name}'
class CityManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('region', 'region__country')
class City(models.Model):
name = models.CharField([...])
region = models.ForeignKey(Region)
objects = CityManager()
def __str__(self):
return f'{self.region.country} - {self.region} - {self.name}'
Hence when I want to display some kind of list of cities (e.g. list all cities in Germany), I have to use select_related to be even remotely efficient otherwise I query for Country each time the __str__ is called. This is not the problem.
The problem is that when I have unrelated group of models and I want to FK to City, such as:
class Tour(models.Model):
[...]
class TourItem(models.Model):
tour = models.ForeignKey(Tour)
city = models.ForeignKey(City)
[...]
Tour would represent a planned tour for some music band; and TourItem would be a specific tour in a given city. I have a simple admin interface for this, so that TourItem is an inline field for the Tour (ie. so multiple tour items can be edited/added simultaneously). The problem is that now there are multiple queries firing for same Country when looking up the City FK and I'm not sure how to solve it. I tried what follows, but it did not work as expected:
class TourManager(models.Manager):
def get_queryset(self):
return super().get_queryset().prefetch_related('touritem_set__city', 'touritem_set__city__region', 'touritem_set__city__region__country')
And neither did this work:
class TourItemManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('city', 'city__region', 'city__region__country')
How can I adjust the managers/models so that when I load Tour's admin there will not be additional queries fired for Country?
You can use ModelAdmin select_related to select related tables
list_select_related = ('author', 'category')
If that is not fully helpful and you still want to do override try with following in your custom Manager
def get_queryset(self, request):
return super(TourItemManager,self).queryset(request).select_related('city', 'city__region', 'city__region__country')

Django autocomplete-light v3 from db

I've already make it directly in HTML page with Jquery but I think isn't a good solution due to the big item list. I think it's better have a huge list stored in the db and than autocompile the field in my case.
I want autocompile (with airport name) two field (departure and destination). So in models.py I make two class:
class Aero(models.Model):
departure = models.CharField(max_length=20)
destination = models.CharField(max_length=20)
class AirportName(models.Model):
air_name= models.CharField(max_length=70)
I populate the db with 3000 airport (I use AirportName class to do that). Now I would like that when a user start to digit in departure or destination field (in a form), it will appear the possibile match airport list. I read the documentation but I don't understand how do that, maybe I mistake everything.
url.py:
url(
r'^fly-autocomplete/$',
Fly.as_view(),
name='fly-autocomplete',
),
views.py:
class Fly(autocomplete.Select2QuerySetView):
def get_queryset(self):
form = AeroForm()
qs = AirportName.objects.all()
if self.q:
qs = qs.filter(name__istartswith=self.q)
return qs
forms.py:
class AeroForm(forms.ModelForm):
class Meta:
model = Aero
fields = ('__all__')
widgets = {
'departure': autocomplete.ModelSelect2(url='fly-autocomplete'),
'destination': autocomplete.ModelSelect2(url='fly-autocomplete'),
}

Custom columns in django_tables2

I've had a search around for this but haven't had much luck so looking for a bit of help. I'm trying to add some extra columns to a table defined by a model, using function definitions in the model. Here's what my code looks like now:
# models.py
class MyModel(models.Model):
my_field = models.TextField()
def my_function(self):
# Return some calculated value based on the entry
return my_value
# tables.py
class MyTable(tables.Table):
my_extra_column = tables.Column(....)
class Meta:
model = MyModel
# views.py
table = MyTable(MyModel.objects.all())
RequestConfig(request).configure(table)
return render(request, ....)
My question is can I access my_function in the entries passed to MyTable so I can show the result of my_function in the custom my_extra_column column? I assume I need to be using accessors, but I can't see how I can access the queryset data using this. Thanks!
I figured it out in the end, it was actually not too hard after all :)
So using my example above, in order to add a custom column using a function in the associated model you just use accessors ...
# models.py
class MyModel(models.Model):
my_field = models.TextField()
my_field_2 = models.IntegerField()
def my_function(self):
# Return some calculated value based on the entry
return my_value
# tables.py
class MyTable(tables.Table):
my_extra_column = tables.Column(accessor='my_function',
verbose_name='My calculated value')
class Meta:
fields = ['my_field', 'my_field_2', 'my_extra_column']
model = MyModel
The trouble comes if and when you want to be able to sort this data, because the function won't translate into any valid field in MyModel. So you could either disable sorting on this column using ordering=False or specify a set using order_by=('field', 'field2')

Using computed fields in admin

I'm trying to use a runtime-computed field in my admin page. This works fine, but I'd like to allow sorting based for that field. Using Django 1.5 (dev), is this possible? I've been scouring the interweb but can't find anything indicating that it is possible.
class Guest(models.Model)
email = models.CharField(max_length=255)
class Invitation(models.Model)
guest = models.ForeignKey(Guest)
created_on = models.DateTimeField(auto_now_add=True)
class GuestAdmin(admin.ModelAdmin):
list_display = ["email", "latest_invitation_sent_on",]
def latest_invitation_sent_on(self, o):
try:
return o.invitation_set.all().order_by(
"-created_on")[0].created_on.strftime("%B %d, %Y")
except IndexError:
return "N/A"
I'd like to be able to enable sorting by latest_invitation_sent_on. Are there any methods of doing this nicely that I'm unaware of?
You should be able to annotate Guests with their latest invitation time and then order_by it (order_by uses the DB to sort and as long as you can provide a valid DB field, table or virtual it should work).
class GuestManager(models.Manager):
def get_query_set(self):
return super(GuestManager, self).get_query_set().annotate(latest_invite=Max("invitation_set__created_on"))
class Guest(models.Model)
email = models.CharField(max_length=255)
objects = GuestManager()
class Invitation(models.Model)
guest = models.ForeignKey(Guest)
created_on = models.DateTimeField(auto_now_add=True)
class GuestAdmin(admin.ModelAdmin):
list_display = ["email", "latest_invite",]
If you only need latest_invite annotation once in a while it makes sense to move it to a separate method or even manager.
class GuestManager(models.Manager):
def by_invitations(self):
return super(GuestManager, self).get_query_set().annotate(latest_invite=Max("invitation_set__created_on")).order_by('-latest_invite')
>>> Guest.objects.by_invitations()