Reload choices dynamically when using MultipleChoiceFilter - django

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.

Related

Add method doesnt work when trying to establish m2m relationships using post_save in Django

My Content model has a many-to-many relationship to the Tag model. When I save a Content object, I want to add the relationships dynamically. I'm doing this the following way.
def tag_content(obj):
for tag in Tag.objects.all():
print tag
obj.tags.add(tag)
obj.is_tagged = True
obj.save()
class Tag(models.Model):
name = models.CharField(max_length=255)
class Content(models.Model):
title = models.CharField(max_length=255)
is_tagged = models.BooleanField(default=False)
tags = models.ManyToManyField(Tag, blank=True)
def save(self, *args, **kwargs):
super(Content, self).save(*args, **kwargs)
#receiver(post_save, sender = Content)
def update_m2m_relationships_on_save(sender, **kwargs):
if not kwargs['instance'].is_tagged:
tag_content(kwargs['instance'])
The tag_content function runs, however, the m2m relationships are not established. Im using Django 1.9.8 btw. This makes no sense. What am I missing? Moreover, if I do something like tag_content(content_instance) in shell, then the tags are set, so the function is ok. I guess the problem is in the receiver. Any help?
Edit
My question has nothing to do with m2m_changed, as I have said, creating a Content object in shell works perfectly. Therefore, the problem lies in the admin panel's setup.
Ok so I solved the problem. Basically, this has something to do with how Django handles its form in the admin panel. When trying to add the Contents from admin, I kept the tags field empty, thinking the tag_content function would handle it. However, that is exactly where the problem was, as creating a Content from shell tagged it just fine. In other words, changing the admin panel to something like this solved my problem :
from django.contrib import admin
from myapp.models import *
from django import forms
class ContentCreationForm(forms.ModelForm):
class Meta:
model = Content
fields = ('title',)
class ContentChangeForm(forms.ModelForm):
class Meta:
model = Content
fields = ('title', 'is_tagged', 'tags')
class ContentAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if obj is None:
return ContentCreationForm
else:
return ContentChangeForm
admin.site.register(Tag)
admin.site.register(Content, ContentAdmin)
When trying to create a new Content, only the 'title' field is presented. This solves the problem.

Django - set filter field label or verbose_name

I'm displaying a table of data using django-tables2.
For filtering I'm using the solution from here:
How do I filter tables with Django generic views?
My problem is only that I can't set the labels for the filter form. This is also imposible to google as words "django, form, filter, label" are quite general :(
My filter class:
import django_filters as filters
from models import Sale
class SaleFilter(filters.FilterSet):
class Meta:
model = Sale
fields = ['CompanyProductID', 'CompanySellerID', 'CompanyRegisterID']
labels = {
'CompanyProductID': 'Article',
'CompanySellerID': 'Seller',
'CompanyRegisterID': 'Cash register'
} #THIS IS NOT WORKING
To set custom labels you can do it this way. Not sure if it is a new functionality.
import django_filters as filters
from models import Sale
class SaleFilter(filters.FilterSet):
CompanyProdutID = filters.CharFilter(label='Article')
CompanySellerID = filters.CharFilter(label='Seller')
CompanyRegisterID = filters.CharFilter(label='Cash register')
class Meta:
model = Sale
fields = ['CompanyProductID', 'CompanySellerID', 'CompanyRegisterID']
Use the filter that you want for each field.
docs
Note:
for some reason
import django_filters as filters
filters.CharField(...)
is not working for me. I have to use it like this:
from django_filters import CharFilter
CharFilter(...)
Previous answer will duplicate the filter fields. Here is how to do it:
def __init__(self, *args, **kwargs):
super(SaleFilter, self).__init__(*args, **kwargs)
self.filters['CompanyProductID'].label="Article"
self.filters['CompanySellerID'].label="Seller"
self.filters['CompanyRegisterID'].label="Cash register"
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['manufacturer']
def __init__(self, *args, **kwargs):
super(ProductFilter, self).__init__(*args, **kwargs)
self.filters['manufacturer'].extra.update(
{'empty_label': 'All Manufacturers'})
I actually believe the op was asking about the "Label name". Not the field name. In order to do this simply do something like the following.
class Name_of_Filter(django_filters.FilterSet):
#example of how to set custom labels
your_field_name = django_filters.WhateverFilterYouWantHere(label='Whatever you want')
class Meta:
model = Your_Model_Here
fields = ['your_field_name']
#could also do something like '__all__' to get all the fields for that table just have to refer to your models to get the field name

DetailView using two ForeignKey kwargs

I am trying to utilize Django's class-based generic DetailView by querying the table using two keyword arguments passed via the url. I have tried overriding both the get_queryset() and get_object() method to no avail. My models look like this (edited for brevity, but let me know if something important is missing):
# models
class Skill(models.Model):
skill = models.CharField()
class User(AbstractBaseUser):
username = models.CharField()
class UserSkills(models.Model):
skill = models.ForeignKey(Skill)
user = models.ForeignKey(User, to_field='username')
value = models.CharField()
my url for the DetailView looks like this:
url(
regex=r"^(?P<username>[a-zA-Z0-9-]{1,25})/skills/(?P<skill>[a-zA-Z0-9 -._/]+)/$",
view=views.UserSkillDetail.as_view(),
name='userskill_detail',
),
the view:
class UserSkillDetail(DetailView):
template_name = 'UserSkill_detail.html'
context_object_name = 'skill'
model = UserSkills
def get_object(self, queryset=None):
get_user = self.kwargs['username']
get_skill = self.kwargs['skill']
return get_object_or_404(UserSkills, user__username=get_user, skill__skill=get_skill)
I keep receiving the following error via the template debug message:
No UserSkills matches the given query.
although I am able to successfully query the following via the shell:
>>> x = UserSkills.objects.get(user__username='user1', skill__skill='skill1')
>>> x
<UserSkills: user1#example.com: skill1>
and have verified that the keyword arguments are being captured correctly ('user1', 'skill1') via the logger and Django debug toolbar. Any help would be greatly appreciated!
There was an issue with my migration sequence (called out of intended order). I reconfigured the migrations and everything worked.

Can I make list_filter in django admin to only show referenced ForeignKeys?

I have a django application which has two models like this:
class MyModel(models.Model):
name = models.CharField()
country = models.ForeignKey('Country')
class Country(models.Model):
code2 = models.CharField(max_length=2, primary_key=True)
name = models.CharField()
The admin class for MyModel looks like this:
class MyModelAdmin(admin.ModelAdmin):
list_display = ('name', 'country',)
list_filter = ('country',)
admin.site.register(models.MyModel, MyModelAdmin)
The Country table contains ~250 countries. Only a handful of countries are actually referenced by some MyModel instance.
The problem is that the list filter in the django admin lists ALL countries in the filter panel. Listing all countries (and not just those that are referenced by an instance) pretty much defeats the purpose of having the list filter in this case.
Is there some to only display the countries referenced by MyModel as choices in the list filter? (I use Django 1.3.)
As of Django 1.8, there is a built in RelatedOnlyFieldListFilter, which you can use to show related countries.
class MyModelAdmin(admin.ModelAdmin):
list_display = ('name', 'country',)
list_filter = (
('country', admin.RelatedOnlyFieldListFilter),
)
For Django 1.4-1.7, list_filter allows you to use a subclass of SimpleListFilter. It should be possible to create a simple list filter that lists the values you want.
If you can't upgrade from Django 1.3, you'd need to use the internal, and undocumented, FilterSpec api. The Stack Overflow question Custom Filter in Django Admin should point you in the right direction.
I know question was about Django 1.3 however you mentioned on soon upgrading to 1.4.
Also to people, like me who was looking for solution for 1.4, but found this entry, i decided to show full example of using SimpleListFilter (available Django 1.4) to show only referenced (related, used) foreign key values
from django.contrib.admin import SimpleListFilter
# admin.py
class CountryFilter(SimpleListFilter):
title = 'country' # or use _('country') for translated title
parameter_name = 'country'
def lookups(self, request, model_admin):
countries = set([c.country for c in model_admin.model.objects.all()])
return [(c.id, c.name) for c in countries]
# You can also use hardcoded model name like "Country" instead of
# "model_admin.model" if this is not direct foreign key filter
def queryset(self, request, queryset):
if self.value():
return queryset.filter(country__id__exact=self.value())
else:
return queryset
# Example setup and usage
# models.py
from django.db import models
class Country(models.Model):
name = models.CharField(max_length=64)
class City(models.Model):
name = models.CharField(max_length=64)
country = models.ForeignKey(Country)
# admin.py
from django.contrib.admin import ModelAdmin
class CityAdmin(ModelAdmin):
list_filter = (CountryFilter,)
admin.site.register(City, CityAdmin)
In example you can see two models - City and Country. City has ForeignKey to Country. If you use regular list_filter = ('country',) you will have all the countries in the chooser. This snippet however filters only related countries - the ones that have at least one relation to city.
Original idea from here. Big thanks to author. Improved class names for better clarity and use of model_admin.model instead of hardcoded model name.
Example also available in Django Snippets:
http://djangosnippets.org/snippets/2885/
Since Django 1.8 there is: admin.RelatedOnlyFieldListFilter
The example usage is:
class BookAdmin(admin.ModelAdmin):
list_filter = (
('author', admin.RelatedOnlyFieldListFilter),
)
I’d change lookups in darklow's code like this:
def lookups(self, request, model_admin):
users = User.objects.filter(id__in = model_admin.model.objects.all().values_list('user_id', flat = True).distinct())
return [(user.id, unicode(user)) for user in users]
This is much better for database ;)
A generalized reusable version of the great #darklow's answer:
def make_RelatedOnlyFieldListFilter(attr_name, filter_title):
class RelatedOnlyFieldListFilter(admin.SimpleListFilter):
"""Filter that shows only referenced options, i.e. options having at least a single object."""
title = filter_title
parameter_name = attr_name
def lookups(self, request, model_admin):
related_objects = set([getattr(obj, attr_name) for obj in model_admin.model.objects.all()])
return [(related_obj.id, unicode(related_obj)) for related_obj in related_objects]
def queryset(self, request, queryset):
if self.value():
return queryset.filter(**{'%s__id__exact' % attr_name: self.value()})
else:
return queryset
return RelatedOnlyFieldListFilter
Usage:
class CityAdmin(ModelAdmin):
list_filter = (
make_RelatedOnlyFieldListFilter("country", "Country with cities"),
)
This is my take on a general and reusable implementation for Django 1.4, if you happen to be stuck at that version. It is inspired by the built-in version that is now part of Django 1.8 and upwards. Also, it should be quite a small task to adapt it to 1.5–1.7, mainly the queryset methods have changed name in those. I've put the filter itself in a core application that I have but you can obviously put it anywhere.
Implementation:
# myproject/core/admin/filters.py:
from django.contrib.admin.filters import RelatedFieldListFilter
class RelatedOnlyFieldListFilter(RelatedFieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
self.request = request
self.model_admin = model_admin
super(RelatedOnlyFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)
def choices(self, cl):
limit_choices_to = set(self.model_admin.queryset(self.request).values_list(self.field.name, flat=True))
self.lookup_choices = [(pk_val, val) for pk_val, val in self.lookup_choices if pk_val in limit_choices_to]
return super(RelatedOnlyFieldListFilter, self).choices(cl)
Usage:
# myapp/admin.py:
from django.contrib import admin
from myproject.core.admin.filters import RelatedOnlyFieldListFilter
from myproject.myapp.models import MyClass
class MyClassAdmin(admin.ModelAdmin):
list_filter = (
('myfield', RelatedOnlyFieldListFilter),
)
admin.site.register(MyClass, MyClassAdmin)
If you later update to Django 1.8 you should be able to just change this import:
from myproject.core.admin.filters import RelatedOnlyFieldListFilter
To this:
from django.contrib.admin.filters import RelatedOnlyFieldListFilter
#andi, thanks for letting know about the fact that Django 1.8 will have this feature.
I took a look on how it was implemented and based on that created version that works for Django 1.7. This is a better implementation than my previous answer, because now you can reuse this filter with any Foreign Key fields. Tested only in Django 1.7, not sure if it works in earlier versions.
Here is my final solution:
from django.contrib.admin import RelatedFieldListFilter
class RelatedOnlyFieldListFilter(RelatedFieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
super(RelatedOnlyFieldListFilter, self).__init__(
field, request, params, model, model_admin, field_path)
qs = field.related_field.model.objects.filter(
id__in=model_admin.get_queryset(request).values_list(
field.name, flat=True).distinct())
self.lookup_choices = [(each.id, unicode(each)) for each in qs]
Usage:
class MyAdmin(admin.ModelAdmin):
list_filter = (
('user', RelatedOnlyFieldListFilter),
('category', RelatedOnlyFieldListFilter),
# ...
)

In django, how to limit choices of a foreignfield based on another field in the same model?

I have these models (I have limited the number of fields to just those needed)
class unit(models.Model):
name = models.CharField(max_length=200)
class project(models.Model):
name = models.CharField(max_length=200)
class location(address):
project = models.ForeignKey(project)
class project_unit(models.Model):
project = models.ForeignKey(project)
unit = models.ForeignKey(unit)
class location_unit(models.Model):
project = models.ForeignKey(project)
#Limit the selection of locations based on which project has been selected
location = models.ForeignKey(location)
#The same here for unit. But I have no idea how.
unit = models.ForeignKey(project_unit)
My newbie head just cannot grasp how to limit the two fields, location and unit, in the location_unit model to only show the choices which refers to the selected project in location_unit. Should I override the modelform and make a query there or can I use the limit_choices_to. Either way I have failed trying both
Edit: Just to clarify, I want this to happen in the Django Admin. I have also tried formfield_for_foreignkey, but still a no go for me.
EDIT 2:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "unit":
kwargs["queryset"] = project_unit.objects.filter(project=1)
return db_field.formfield(**kwargs)
return super(location_unit_admin, self).formfield_for_foreignkey(db_field, request, **kwargs)
The above code snippet works. But of course I don't want the project to point to 1. How do I reference to the models project_id?
I tried this:
kwargs["queryset"] = project_unit.objects.filter(project=self.model.project.project_id)
But that doesn't work (actually I have tried a lot of variations, yes I am a django newbie)
This is the answer, it is brilliant: https://github.com/digi604/django-smart-selects
Your formfield_for_foreignkey looks like it might be a good direction, but you have to realize that the ModelAdmin (self) won't give you a specific instance. You'll have to derive that from the request (possibly a combination of django.core.urlresolvers.resolve and request.path)
If you only want this functionality in the admin (and not model validation in general), you can use a custom form with the model admin class:
forms.py:
from django import forms
from models import location_unit, location, project_unit
class LocationUnitForm(forms.ModelForm):
class Meta:
model = location_unit
def __init__(self, *args, **kwargs):
inst = kwargs.get('instance')
super(LocationUnitForm, self).__init__(*args, **kwargs)
if inst:
self.fields['location'].queryset = location.objects.filter(project=inst.project)
self.fields['unit'].queryset = project_unit.objects.filter(project=inst.project)
admin.py:
from django.contrib import admin
from models import location_unit
from forms import LocationUnitForm
class LocationUnitAdmin(admin.ModelAdmin):
form = LocationUnitForm
admin.site.register(location_unit, LocationUnitAdmin)
(Just wrote these on the fly with no testing, so no guarantee they'll work, but it should be close.)