Show distinct values in dropdown menu with django-select2 - django

I am using django-select2 with forms for creating a drop-down list, using ModelSelect2Widget.
My problem is that I filter on the field 'name' and several objects can have the same 'name' but different 'value' (see model description below). This leads to the same name appearing several times in the dropdown menu.
I would like to remove these duplicates. I tried to use .distinct('name') for selecting the queryset, but it doesn't seem to work.
example of result obtained with ModelSelect2Widget
Below is the description of the code I use:
I have two models linked by a foreign key
models.py
class Who(models.Model):
name = models.CharField(max_length=256)
value = models.CharField(max_length=256)
def __str__(self);
return str(self.name)
class Data(models.Model):
who = models.ForeignKey(Who)
And I use the form described here:
forms.py
from django_select2.forms import ModelSelect2Widget
...
class CreateDataForm(ModelForm):
class Meta:
model = Data
fields = ('who',)
widgets = {'who': ModelSelect2Widget(
queryset=Who.objects.all().distinct('name'),
search_fields=['name_icontains']
)}
Does anyone know how I could remove these duplicates?

I finally found the source of the problem.
In django_select2 ModelSelect2Mixin, the function filter_queryset which is used to filter the values to show on the dropdown menu ends by "return queryset.filter(select).distinct()". This problem is that this .distinct() prevails on the one from the queryset (.distinct("name"))
In order to solve my problem, I had to override the filter queryset function, and remove the .distinct():
class ModelSelect2WidgetWithoutDistinct(ModelSelect2Widget):
"""In dropdown list, shows objects from queryset without the initial .distinct()."""
def filter_queryset(self, request, term, queryset=None, **dependent_fields):
"""Same function as in select2 ModelSelect2Mixin except last line."""
if queryset is None:
queryset = self.get_queryset()
search_fields = self.get_search_fields()
select = Q()
term = term.replace('\t', ' ')
term = term.replace('\n', ' ')
for t in [t for t in term.split(' ') if not t == '']:
select &= reduce(lambda x, y: x | Q(**{y: t}), search_fields,
Q(**{search_fields[0]: t}))
if dependent_fields:
select &= Q(**dependent_fields)
return queryset.filter(select) # initially queryset.filter(select).distinct()

# You can define queryset like this
class CreateDataForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['who'].queryset = Who.objects.distinct()
class Meta:
model = Data
fields = ('who',)

Related

DJANGO , custom LIST_FILTER

I am using only the django admin , and trying to creating a custom filter, where is to filter the date of another model.
My models
class Avaria(models.Model):
.....
class Pavimentacao(models.Model):
avaria = models.ForeignKey(Avaria, related_name='AvariaObjects',on_delete=models.CASCADE)
date= models.DateField(blank=True,null=True)
AvariaAdmin
class AvariaAdmin(admin.ModelAdmin):
list_filter = ('')
For example, Let's say you have a model and you have to add custom ContentTypeFilter to your model admin then. you can define a class which inherit SimpleListFilter and define lookups and queryset based on your requirement and add this class to list_filter like
list_filter = [ContentTypeFilter]
Refer to docs
Example class definition is like below:
class ContentTypeFilter(admin.SimpleListFilter):
# Human-readable title which will be displayed in the
# right admin sidebar just above the filter options.
title = _('content type')
# Parameter for the filter that will be used in the URL query.
parameter_name = 'type'
def lookups(self, request, model_admin):
"""
Returns a list of tuples. The first element in each
tuple is the coded value for the option that will
appear in the URL query. The second element is the
human-readable name for the option that will appear
in the right sidebar.
"""
models_meta = [
(app.model._meta, app.model.__name__) for app in get_config()
]
return (item for item in models_meta)
def queryset(self, request, queryset):
"""
Returns the filtered queryset based on the value
provided in the query string and retrievable via
`self.value()`.
"""
if not self.value():
return
model = apps.get_model(self.value())
if model:
return queryset.models(model)
You have to add the field you want to filter. In your example if you want to filter on date you put list_filter('date'). Dont forget to register the model admin as seen here

Django Rest Framework categories and childs in one model

I have a very simple ( with a first look) problem. Case - A product can be sold in a several places(shops), and every product can be represented in a single shop with a different categories and sub categories ( That is why category linked via ForeignKey with Assortment twice).
So here is My Assortment model, with several FKs.
class Assortment(models.Model):
category = models.ForeignKey('category.Category', null=True, blank=True, default=None,related_name='assortment_child')
parent_category = models.ForeignKey('category.Category', null=True, blank=True, default=None,related_name='assortment_parent')
product = models.ForeignKey(Product)
shop = models.ForeignKey(Shop)
View, based on rest_framework.generics.ListAPIView
class InstitutionTreeCategories(generics.ListAPIView):
"""Resource to get shop's tree of categories."""
serializer_class = serializers.InstitutionCategoriesSerializer
def get_queryset(self):
shop = self.get_shop()
return Category.objects.filter(assortment_parent__shop=shop).distinct()
And finally, serializers
class CategoryListSerializer(serializers.ModelSerializer):
class Meta:
"""Meta class."""
model = Category
fields = ('id', 'name', 'image')
class CategoriesTreeSerializer(CategoryListSerializer):
# childs = CategoryListSerializer(many=True, source='assortment_child__parent_category')
childs = serializers.SerializerMethodField()
class Meta(CategoryListSerializer.Meta):
"""Meta class."""
fields = ('id', 'name', 'image', 'childs')
def get_childs(self, obj):
qs = Category.objects.filter(assortment_child__parent_category=obj.id).distinct()
return CategoryListSerializer(qs, many=True, context=self.context).data
And i need to show Category Tree for a one single shop with my API.
But the problem is - If I use serializer.SerializerMethodField - it works, but too many queries (for every parent category). I tried to avoid it using 'source' option with my 'CategoryListSerializer' by I can't make it. Every time, I get - 'Category' object has no attribute assortment_child__parent_category. In a shell model i've tried
In [8]: cat.assortment_parent.values('category').distinct()
Out[8]: (0.003) SELECT DISTINCT "marketplace_assortment"."category_id" FROM "marketplace_assortment" WHERE "marketplace_assortment"."parent_category_id" = 4 LIMIT 21; args=(4,)
<AssortmentQuerySet [{'category': 3}]>
So - category object has this attributes, of course it does, i used it a get_childs method. So question is - how i can use it with serializer.ModelSerializer and it's source option? ( Of course using select_related method with queryset, to avoid excess queries).
by source option you should use . in instead of __:
childs = CategoryListSerializer(many=True, source='assortment_child.parent_category')
but still you will has many queries, to fix it you should use prefetch-related
def get_queryset(self):
shop = self.get_shop()
qs = Category.objects.filter(assortment_parent__shop=shop).all()
return qs.prefetch_related('assortment_child').distinct()
more detail you can read in the how-can-i-optimize-queries-django-rest-framework
I had the similar problem and the best solution I have found is to do some manual processing in order to receive desired tree representation.
So firstly we fetch all Assortment for shop and then build the tree manually.
Let's look at the example.
def get_categories_tree(assortments, context):
assortments = assortments.select_related('category', 'parent_category')
parent_categories_dict = OrderedDict()
for assortment in assortments:
parent = assortment.parent_category
# Each parent category will appear in parent_categories_dict only once
# and it will accumulate list of child categories
if parent not in parent_categories_dict:
parent_data = CategoryListSerializer(instance=parent, context=context).data
parent_categories_dict[parent] = parent_data
parent_categories_dict[parent]['childs'] = []
child = assortment.category
child_data = CategoryListSerializer(instance=child, context=context).data
parent_categories_dict[parent]['childs'].append(child_data)
# convert to list as we don't need the keys already - they were used only for matching
parent_categories_list = list(parent_categories_dict.values())
return parent_categories_list
class InstitutionTreeCategories(generics.ListAPIView):
def list(self, request, *args, **kwargs):
shop = self.get_shop()
assortments = Assortment.objects.filter(shop=shop)
context = self.get_serializer_context()
categories_tree = get_categories_tree(assortments, context)
return Response(categories_tree)
All in single DB query.
The problem here is that there is no explicit relation between category and parent_category. If you define ManyToManyField in Category using Assortment as through intermediate model, you will get an access which Django can understand, so you would just use attribute childs on Category for example. However this will still return all children (the same would happen if your source example works) categories, ignoring shop, so some clever Prefetch would have to be done to achieve correct results. But I believe manual "join" is simpler.
you need to use prefetch_related along with serializer method field
serializer:
class CategoriesTreeSerializer(CategoryListSerializer):
children = serializers.SerializerMethodField()
class Meta(CategoryListSerializer.Meta):
fields = (
'id',
'name',
'image',
'children'
)
def get_children(self, obj):
children = set()
for assortment in obj.assortment_parent.all():
children.add(assortment.category)
serializer = CategoryListSerializer(list(children), many=True)
return serializer.data
your get queryset method:
def get_queryset(self):
shop = self.get_shop()
return (Category.objects.filter(assortment_parent__shop=shop)
.prefetch_related(Prefetch('assortment_parent', queryset=Assortment.objects.all().select_related('category')
.distinct())

How to Customize Django Admin Filter

Using django 1.7.7 admin page, I want to list the data in a table where I can sort and filter.
That's exactly what the picture on the documentation shows:
https://docs.djangoproject.com/en/1.7/ref/contrib/admin/
under the list_filter section.
I tried that and got an error:
The value of 'list_display[0]' refers to 'book_type', which is not a callable.
Then, I tried this:
# model.py:
class Interp():
""" specifies the interpretation model """
BOOK_TYPES = ['drama', 'scietific']
BOOK_TYPES = tuple(zip(BOOK_TYPES, BOOK_TYPES))
comment = models.CharField(max_length=20)
book_type = models.CharField(max_length=20, choices=BOOK_TYPES,
default='not specified')
def __unicode__(self):
return self.book_type
# admin.py:
class bookTypeFilter(SimpleListFilter):
title = 'book type'
parameter_name = 'book_type'
def lookups(self, request, model_admin):
types = set([t.book_type for t in model_admin.model.objects.all()])
return zip(types, types)
def queryset(self, request, queryset):
if self.value():
return queryset.filter(book_type__id__exact=self.value())
else:
return queryset
class AdminInterpStore(admin.ModelAdmin):
""" admin page setting """
search_fields = ('comment', 'book_type')
list_display = ('book_type',)
list_filter = (bookTypeFilter, )
this shows a sidebar and lists the values but as soon as I click any of them, I get an error:
FieldError: Unsupported lookup 'id' for CharField or join on the field not permitted
How can I make a filter, preferably a table view as well?
Is the a complete sample for django1.7.7 that I can look at for filtering admin page?
You must replace
return queryset.filter(book_type__id__exact=self.value())
by
return queryset.filter(book_type=self.value())
See https://docs.djangoproject.com/en/1.8/ref/models/querysets/#field-lookups for more information.
Also, in the lookups method, using:
types = model_admin.model.objects.distinct('book_type').values_list('book_type', flat=True)
would be much more efficient as you would not retrieve the whole data from the database to then filter it.
Edit:
I realized that the book_type field is a choice field.
First, the value passed to the choices argument must be
An iterable (e.g., a list or tuple) consisting itself of iterables of exactly two items (e.g. [(A, B), (A, B) ...] A being the value stored in the database, B a human readable value)
So passing a list of strings will fail.
Second, the Django admin's default behaviour for such fields is exactly what you are trying to achieve so setting list_filter = ('book_type', ) in AdminInterpStore is enough.

How to modify form choices from class?

I have form class:
class Form(forms.ModelForm):
id = forms.ModelChoiceField(queryset=Option.objects.all(), widget=forms.HiddenInput())
category = forms.ModelChoiceField(queryset=Category.objects.all())
class Meta:
model = Option
fields = ('id', 'category')
def choices(self, ext_data):
# something with extdata...
choices = [('1','one')]
category = forms.ModelChoiceField(queryset=choices)
but this:
my_form.choices(something)
is not working. Why?
I must implement this in class because i have one view and many different forms. Each form have specific choices function.
First, queryset must be a queryset, not a list, since you're using ModelChoiceField. Second, to reference the category form field use self.fields['category']. Your function should thus look something like this:
def choices(self, ext_data):
#I'm not sure what ext_data is, but I suspect it's something to filter the Categories
self.fields['category'].queryset = Category.objects.filter(something=ext_data)
#If ext_data itself is a queryset you can use it directly:
self.fields['category'].queryset = ext_data
For clarification, a queryset is what you get when you use Model.objects.filter(xxx) or any other filtering action on your model.
Try to use init:
class MessageAdminForm(forms.ModelForm):
def __init__(self, *arg, **kwargs):
super(MessageAdminForm, self).__init__(*args, **kwargs)
# set choices this way
self.fields['field'].choices = [(g.id, g) for g in something]

field choices() as queryset?

I need to make a form, which have 1 select and 1 text input. Select must be taken from database.
model looks like this:
class Province(models.Model):
name = models.CharField(max_length=30)
slug = models.SlugField(max_length=30)
def __unicode__(self):
return self.name
It's rows to this are added only by admin, but all users can see it in forms.
I want to make a ModelForm from that. I made something like this:
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=CHOICES),
}
but it doesn't work. The select tag is not displayed in html. What did I wrong?
UPDATE:
This solution works as I wanto it to work:
class ProvinceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProvinceForm, self).__init__(*args, **kwargs)
user_provinces = UserProvince.objects.select_related().filter(user__exact=self.instance.id).values_list('province')
self.fields['name'].queryset = Province.objects.exclude(id__in=user_provinces).only('id', 'name')
name = forms.ModelChoiceField(queryset=None, empty_label=None)
class Meta:
model = Province
fields = ('name',)
Read Maersu's answer for the method that just "works".
If you want to customize, know that choices takes a list of tuples, ie (('val','display_val'), (...), ...)
Choices doc:
An iterable (e.g., a list or tuple) of
2-tuples to use as choices for this
field.
from django.forms.widgets import Select
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=( (x.id, x.name) for x in CHOICES )),
}
ModelForm covers all your needs (Also check the Conversion List)
Model:
class UserProvince(models.Model):
user = models.ForeignKey(User)
province = models.ForeignKey(Province)
Form:
class ProvinceForm(ModelForm):
class Meta:
model = UserProvince
fields = ('province',)
View:
if request.POST:
form = ProvinceForm(request.POST)
if form.is_valid():
obj = form.save(commit=True)
obj.user = request.user
obj.save()
else:
form = ProvinceForm()
If you need to use a query for your choices then you'll need to overwrite the __init__ method of your form.
Your first guess would probably be to save it as a variable before your list of fields but you shouldn't do that since you want your queries to be updated every time the form is accessed. You see, once you run the server the choices are generated and won't change until your next server restart. This means your query will be executed only once and forever hold your peace.
# Don't do this
class MyForm(forms.Form):
# Making the query
MYQUERY = User.objects.values_list('id', 'last_name')
myfield = forms.ChoiceField(choices=(*MYQUERY,))
class Meta:
fields = ('myfield',)
The solution here is to make use of the __init__ method which is called on every form load. This way the result of your query will always be updated.
# Do this instead
class MyForm(forms.Form):
class Meta:
fields = ('myfield',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the query here
MYQUERY = User.objects.values_list('id', 'last_name')
self.fields['myfield'] = forms.ChoiceField(choices=(*MYQUERY,))
Querying your database can be heavy if you have a lot of users so in the future I suggest some caching might be useful.
the two solutions given by maersu and Yuji 'Tomita' Tomita perfectly works, but there are cases when one cannot use ModelForm (django3 link), ie the form needs sources from several models / is a subclass of a ModelForm class and one want to add an extra field with choices from another model, etc.
ChoiceField is to my point of view a more generic way to answer the need.
The example below provides two choice fields from two models and a blank choice for each :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=([['','-'*10]]+[[x.id, x.__str__()] for x in Speakers.objects.all()]))
event = forms.ChoiceField(choices=( [['','-'*10]]+[[x.id, x.__str__()] for x in Events.objects.all()]))
If one does not need a blank field, or one does not need to use a function for the choice label but the model fields or a property it can be a bit more elegant, as eugene suggested :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=((x.id, x.__str__()) for x in Speakers.objects.all()))
event = forms.ChoiceField(choices=(Events.objects.values_list('id', 'name')))
using values_list() and a blank field :
event = forms.ChoiceField(choices=([['','-------------']] + list(Events.objects.values_list('id', 'name'))))
as a subclass of a ModelForm, using the one of the robos85 question :
class MixedForm(ProvinceForm):
speaker = ...