Multiple choice form within an advanced search form - django

I am trying to create a multiple choice form where any combination of languages can be chosen. It's within a search form field:
class AdvancedSearchForm(SearchForm):
terms_show_partial_matches = forms.BooleanField(required=False,
label=_("Show partial matches in terms")
)
definitions_show_partial_matches = forms.BooleanField(required=False,
label=_("Show partial matches in definitions")
)
case_sensitive = forms.BooleanField(required=False,
label=_("Case sensitive")
)
...
I would like to implement something like this:
filter_by_part_of_speech = forms.ModelChoiceField(
queryset=PartOfSpeech.objects.all(), required=False,
label=_("Filter by part of speech")
)
However, it needs to be a multiple choice field so that any of the values can be chosen. Ideally though, I'm looking for a form where checkboxes are already checked. So something like this:
LANG_CHOICES = (
("1", "lang1"),
("2", "lang2"),
("3", "lang3"),
("4", "lang4"),
)
filter_by_language = forms.MultipleChoiceField(choices=Language.objects.all().filter(name__in=LANG_CHOICES).values(), required=False, label=_("Filter by language"))
The filter is called from the view with something like this:
tqs = tqs.filter(language=language_filter)
Now although the search works fine, the values are not displayed. On the other hand, they are displayed if I fill up a list and simply write:
choices=list(lang_list)
But then, obviously, the search is not actually performed.
Therefore, my questions are:
Can the constructor be adapted to display the values correctly?
Should I rather implement the filter in the view? If so, how?
Am I using the correct type of form or are there better options, such as providing a list of checkboxes that are checked by default?
I am using Django 2.2 (planning to upgrade soon) for now.
The template file simply refers to the search def in the view, which calls the advanced search form and the others:
{% block breadcrumbs_item %}{% trans "Advanced Search" %}{% endblock %}
Not that relevant I think, but here is the Language model:
class Language(models.Model):
iso_code = models.CharField(primary_key=True, max_length=10, verbose_name=_("ISO code"))
name = models.CharField(max_length=50, verbose_name=_("name"))
description = models.TextField(verbose_name=_("description"), null=True, blank=True)
class Meta:
verbose_name = _("language")
verbose_name_plural = _("languages")
def __str__(self):
return _("%(language_name)s (%(iso_code)s)") % {'language_name': self.name, 'iso_code': self.iso_code}
EDIT: Clarification based on Milo Persic's reply.
The request method in the view for the search functionality (that includes calling AdvancedSearchForm) is GET.
if request.method == 'GET' and 'search_string' in request.GET:
if "advanced" in request.path:
search_form = AdvancedSearchForm(request.GET)
else:
search_form = SearchForm(request.GET)
if search_form.is_valid():
tqs = Translation.objects.all()
dqs = Definition.objects.all()
data = search_form.cleaned_data
...
language_filter = data['filter_by_language']
case_filter = data['case_sensitive']
qs = apply_filters(tqs, dqs, language_filter, case_filter, orig_search_string)
etc.
The search form and results are appended and returned as context to the template:
return render(request, template_name, context)
Within AdvancedSearchForm, variables using ModelChoiceField as well as BooleanField are defined. The ModelChoiceField is the one I'm trying to replace with something more user friendly:
filter_by_language = forms.ModelChoiceField(
queryset=Language.objects.all(), required=False,
label=_("Filter by language")
)
The idea is that the default search result will include all four languages (it won't be more than that) but that any combination of them can be unchecked if so desired. With ModelChoiceField, it seems that only one can be chosen.
I'm still trying to learn if ModelMultipleChoiceField is the correct choice, but this is what I have so far:
filter_by_language = forms.ModelMultipleChoiceField(required=True, queryset=Language.objects.all().filter(name__in=LANG_CHOICES).values(),
widget=forms.CheckboxSelectMultiple)
Nothing next to "filter by language".
Only using the names in LANG_CHOICES instead of the numbers actually result in something but it shows the query set. Narrowing it down using "value_list('name')" shows only the language name but still with the syntax, e.g. "('English',)".
mmcf_qs = Language.objects.all().filter(name__in=LANG_CHOICES).values_list('name')
filter_by_language = forms.ModelMultipleChoiceField(required=True, queryset=mmcf_qs)
I'm trying to figure out how to extract the text but maybe this isn't the right approach.

I would use something like CheckBoxSelectMultiple in the form:
(disclaimer, I do not know if this is supported as written in django 2.2)
filter_by_language = forms.ModelMultipleChoiceField(required=True,
queryset=Language.objects.all().filter(name__in=LANG_CHOICES).values(),
widget=forms.CheckboxSelectMultiple)
And there are plenty of stack posts on how to make the initial values checked, like this one.
Also, if you end up with a long list of check boxes (a likely scenario using this widget), you can style them into columns for better UX, like so:
<style>
.my-language-checkboxes {
column-width: 25%;
column-count: 4;
column-fill: balance;
}
</style>
<div id="my-language-checkboxes">
{{ form.filter_by_language }}
</div>
As for the view, I'll make an assumption that you know how to write a basic view and it's the save method you need. In that case, you'll need something like:
if request.method == 'POST:
if form.is_valid():
form.save_m2m()
This also assumes that you are relating languages back to a parent object via a ManyToManyField, which is a common scenario.

Related

if statement for a specific field in django template

I'm rendering a form in a django template using this model:
class Group(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
members = models.IntegerField(default=0)
has_max = models.BooleanField(default=False)
max_size = models.IntegerField(default=10)
DAYS = [
('Sundays', 'Sundays'),
('Mondays', 'Mondays'),
('Tuesdays', 'Tuesdays'),
('Wednesdays', 'Wednesdays'),
('Thursdays', 'Thursdays'),
('Fridays', 'Fridays'),
('Saturdays', 'Saturdays'),
]
meeting_day = MultiSelectField(
verbose_name = 'Meeting day(s)',
choices=DAYS,
max_choices=6,
max_length=100
)
start_time = TimeField(widget=TimePickerInput)
end_time = TimeField(widget=TimePickerInput)
def __str__(self):
return self.name
And onto this template:
<form method="POST">
{% csrf_token %}
{{form.as_p}}
<button>POST</button>
</form>
I have a couple of issues going forward:
My first issue is that I only want to have max_size be displayed in my template form if the user clicks has_max. Like a conditional, where once the user checks the box, changing has_max to True then they can enter in the max size of the group.
My second issue is that the start_time and end_time to render in my template or on the admin side. I'm not sure how TimePickerInput works, but right now, there are no fields in my template form or admin form that have those time fields.
Also, last thing, if I have both names the exact same (i.e., ('Sundays', 'Sundays'), is it necessary to have both? Or can django figure it out.
The first Problem as I understand it You want to have this check box and when it will true the Field of max_size will appear this problem needs to solve with javascript
Why ?
Because if You use the Django template you will need to refresh the page when the user changes something and this will overload the server So you need to create an event with js when the use click on the checkbox it will disappear
Also, last thing, if I have both names the exact same (i.e., ('Sundays', 'Sundays'), is it necessary to have both? Or can Django figure it out?
This part ... You need to know that the first value is the value will be stored in the database and the second value is the human-readable value this what will be displayed for the user in the form Read This
You must know you can't put widgets into your model if you want to add it add it to your from in Your admin or form files ... You will override the AdminModel in your admin file and change the formvalue Read This

Queryset for django-select2 widget being ignored

In my project I am using Django-Select2 heavily, particularly its ModelSelect2Widget as my users frequently need to select from lists of 2,000-6,000 items. In all my uses of it up 'til now, the queryset for the widget has always been called as ".all()" instances of a model, for the user to select from, and there's been no issues.
Now, however, I have implementations in different parts of the project for which filtering the queryset of options for the widget is necessary. In all of these cases, however, any modification of the queryset seems to have no effect, and I'm wondering if there is a problem with the widget itself.
In the primary case, the items in the database are boolean-flagged as active/inactive (about 65% inactive), and I need to only have active items available for the end-user to select.
I'm able to filter the queries correctly via the shell.
In the form definitiion, any filtering (".filter(flag_active=True)", or even setting the queryset to ".none()" has no effect – there is no apparent change in the options in the dropdown/autocomplete. Being that it is a select2 input, I can only view a small number of items at a time, but both the initial retrieved population and the winnowed-down selection as I type indicate that the filters are not been followed.
MODEL:
class Inventory_product_base(models.Model):
id = models.UUIDField(primary_key=True,default=uuid.uuid4,null=False)
upc = models.CharField(max_length=96,null=True,blank=True)
name = models.CharField('Item name',max_length=96,null=False)
flag_active = models.BooleanField("Active item",default=True)
price = models.DecimalField(max_digits=8,decimal_places=3,null=True,blank=True)
unit_of_measure = models.CharField('UOM',max_length=24, choices=UNITS_OF_MEASURE,default='EACH')
spec = models.CharField(max_length=36,null=True,blank=True)
category = models.ForeignKey(Inventory_category,on_delete=models.CASCADE,related_name='cat_products')
subcategory = models.ForeignKey(Inventory_subcategory,on_delete=models.CASCADE,related_name='subcat_products')
note = models.CharField(max_length=275,null=True,blank=True)
def __str__(self):
return str(self.name)
FORM:
class InventoryCatalogUpdateProductsForm(forms.ModelForm):
parent_product_base = forms.ModelChoiceField(
queryset=Inventory_product_base.objects.filter(flag_active=True),
label=u"",
widget=ModelSelect2Widget(
model=Inventory_product_base,
search_fields=['name__icontains'],
attrs={'data-placeholder': 'Select product...', 'data-width': '100%'},),)
class Meta():
model = Inventory_unit_catalog
fields = ('parent_product_base',)
class InventoryCatalogUpdateAllProductsForm(forms.ModelForm):
parent_product_base = forms.ModelChoiceField(
queryset=Inventory_product_base.objects.all(),
label=u"",
widget=ModelSelect2Widget(
model=Inventory_product_base,
search_fields=['name__icontains'],
attrs={'data-placeholder': 'Select product...', 'data-width': '100%'},),)
class Meta():
model = Inventory_unit_catalog
fields = ('parent_product_base',)
InventoryCatalogUpdateProductsFormset = modelformset_factory(model=Inventory_unit_catalog,form=InventoryCatalogUpdateProductsForm,extra=10,can_delete=True)
InventoryCatalogUpdateAllProductsFormset = modelformset_factory(model=Inventory_unit_catalog,form=InventoryCatalogUpdateAllProductsForm,extra=10,can_delete=True)
VIEW:
if product_flag == 'active':
formset = InventoryCatalogUpdateProductsFormset(queryset=parent_unit_catalog.products.filter(flag_active=True))
else:
formset = InventoryCatalogUpdateAllProductsFormset(queryset=parent_unit_catalog.products.all())
As noted, if I change the above queryset to .none() (or anything else, either in the widget or in the view) there is no difference in the rendered choices in the select2 field.
I've tried separate, parallel forms and formsets. Originally I tried for a more sophisticated approach, to pass a parameter and have the different querysets selected within a single form, by adding the following:
def __init__(self, *args, **kwargs):
self.product_flag = kwargs.pop('product_flag')
super(InventoryCatalogAddToForm, self).__init__(*args, **kwargs)
print("__init__ has product_flag: ",self.product_flag)
if self.product_flag == 'active':
self.fields['parent_product_base'].queryset = Inventory_product_base.objects.filter(flag_active=True)
print("Screened for flag_active=True")
else:
self.fields['parent_product_base'].queryset = Inventory_product_base.objects.all()
print("Screened for flag_active=False")
and I was able to verify by the debug prints that the correct filter choices were executing, but without any effect. So I moved back to a simpler, more direct approach of separate forms, and still nothing.
Any advice would be welcome. My project is several months in and Django-Select2 is one of the foundations across it, I would hate to learn that it cannot filter the select2 input and I would need to find a replacement.
self.fields['parent_product_base'].queryset sets the queryset for the formfield (i.e. allowed choices for validation).
Use self.fields['parent_product_base'].widget.queryset to set the widget's choices.

Is it possible to use Model manager to filter directly from template?

I have created a model manager to be able to filter data
class BOMVersion_default_active_Manager(models.Manager):
def get_queryset(self):
return super(BOMVersion_default_active, self).get_queryset().filter(is_default=True,is_active=True)
#with_author
class BOMVersion(models.Model):
version = IntegerVersionField( )
name = models.CharField(max_length=200,null=True, blank=True)
description = models.TextField(null=True, blank=True)
material = models.ForeignKey(Material)
default_active_objects = BOMVersion_default_active_Manager()
I try to use it in my nested for loop from template ( since I cant filter directly in template and this is how i decided to overcome this limitation)
{% for bomversion in soproduct.product.material.bomversion.default_active_objects_set.all %}
But I am not getting any output. What could be the problem? Can I do it in general?
Using a custom Manager is not the right way to go. You need to carefully read the whole Django Managers article.
In this situation what you need is a custom QuerySet. Something like this:
class BOMVersionQuerySet(models.QuerySet):
def active(self):
return self.filter(is_active=True)
def default(self):
return self.filter(is_default=True)
#with_author
class BOMVersion(models.Model):
version = IntegerVersionField( )
name = models.CharField(max_length=200,null=True, blank=True)
description = models.TextField(null=True, blank=True)
material = models.ForeignKey(Material)
objects = BOMVersionQuerySet.as_manager()
Now you can use .active() and .default() methods to filter any BOMVersion-QuerySet. This is actually what you got when you use the reverse relation from Material model, material.bomversion_set is a BOMVersion-QuerySet and thus you don't have access to BOMVersion.objects, but since its a BOMVersion-QuerySet you can use .active() and .default()
{% for bomversion in soproduct.product.material.bomversion_set.default.active %}
However, again this is completely wrong and my advice is not to do it in the template. Use the view to build you querysets, and only iterate them in the template.
Why its bad in the template? Because right now you are making 1 query per material object, and there is no way to optimize it unless you use prefetch_related, but guess what? You can't use prefetch_related in the django template system. (Its on purpose, and the reason is to not do stuff like that in the template), so the correct approach is to make something similar to this:
#in the view:
soproduct = SoProduct.objects.select_related('product__material').prefetch_related(
Prefetch(
'product__material__bomversion_set',
queryset=BOMVersion.objects.default().active()
to_attr='default_active_bomversions'
)
).get(pk=soproduct_id)
#in the template:
{% for bomversion in soproduct.product.material.default_active_bomversions %}
Now you are gonna make 1 query for the soproduct with its related product and material data and 1 more query for the requested default_active_bomversions.

Django - show in template related class count filtered by parameter

I will give my models first and then write description.
class Entry(models.Model):
entry_text = models.TextField()
class Category(models.Model):
user = models.ForeignKey(User)
category_text = models.CharField(max_length=200)
entries = models.ManyToManyField(Entry, through='CategoryEntry')
class CategoryEntry(models.Model):
category = models.ForeignKey(Category)
entry = models.ForeignKey(Entry)
viewed = models.BooleanField(default=False)
So I have Entry model and Category model, and I have created intermediate model CategoryEntry as descriebed here https://docs.djangoproject.com/en/1.7/topics/db/models/#extra-fields-on-many-to-many-relationships because I need one extra field "viewed" (marked as True when user for the first time opens specific Entry link).
So I have created generic.ListView view, where I show all these categories that user has created for himself. What I want, is to show next to every category name, how many entries there are and how many entries he hasn't viewed yet.
Like:
Category Total Not_viewed
AAA 126 5
BBB 17 15
I have managed to show total entries in template by
{% for category in categories %}
{{ category.text }}
{{ category.entries.count }}
{% endfor %}
In my view I have get_queryset like
def get_queryset(self):
categories = Category.objects.filter(user=self.request.user.id)[:]
return categories
As I understand, then the best way would somehow add this extra info about every categories entries viewed count in get_queryset. I have searched around but didn't found anything what works. Have tried some things with select_related, prefetch_related, annotate but don't get whats the right way to do this.
Know that it's not right, but tried something like that and some other things.
categories = Category.objects.filter(user=self.request.user.id).select_related('categoryentry').filter(categoryentry__viewed=False).count()
categories = Category.objects.filter(user=self.request.user.id).annotate(not_viewed_count=Count('categoryentry')).filter(not_viewed_count__viewed=False)
Hope you get my idea what I wan't to achieve.
In your CategoryEntry model, use related_name in the category field like so:
category = models.ForeignKey(Category, related_name="related_entry_categories")
Now you can use this related name when querying the Category model. For example:
from itertools import chain
categories_not_viewed = Category.objects.filter(user=self.request.user.id, related_entry_categories__viewed=False).annotate(num_not_viewed=Count('related_en‌​try_categories'))
categories_viewed = Category.objects.filter(user=self.request.user.id, related_entry_categories__viewed=True).extra(select={'num_not_viewed': 0})
categories = chain(list(categories_not_viewed), list(categories_viewed))
At end I came up with this solution:
categories = Category.objects.filter(user=self.request.user.id).extra(select = {
"num_not_viewed" : """
SELECT COUNT(*)
FROM app_categoryentry
WHERE app_categoryentry.category_id = app_category.id
AND app_categoryentry.viewed = %d """ % 0,
})
Based on the solution from this resource http://timmyomahony.com/blog/filtering-annotations-django/
If anyone have other solution how the get the same result with only Django ORM, I would like to know.

django model search form

Firstly, I did my homework and looked around before posting! My question seems like a very basic thing that must’ve been covered before.
I'm now looking at Django-filter as a potential solution, but would like some advice on if this is the right way to go and if there any other solutions.
I have a Django app wit 10 models, each model has a few fields. Most fields are ChoiceField that users populate using forms with the default select widget. There is a separate form for each model.
I want to create a separate form for each model (in separate views) that users will use to search the database. The search form will contain only drop-down boxes (the select widgets) with the same choices as the forms used to populate the database with the addition of the “any” option.
I know how to use .object.filter(), however the “any” option would correspond to not include specific fields in the filter and I'm not sure how to add model fields to the filter based on users’ selection
I briefly looked at Haystack as an option but it seems to be made for full text search rather than “model filed search” I'm after.
Sample model (simplified):
class Property():
TYPE_CHOICES = (‘apartment’, ‘house’, ‘flat’)
type = charfield(choices=TYPE_CHOICES)
LOC_CHOICES = (‘Brussels’, ‘London’, ‘Dublin’, ‘Paris’)
location = charfield(choices=LOC_CHOICES)
price = PostivieInteger()
Users can select only “type”, only “location” or both (not making selection is equal to ANY) in which case I end up with 3 different filters:
Property.objects.filter(type=’apartment’)
Property.objects.filter(location=’Dublin’)
Property.objects.filter(type=’apartment’, location=’Dublin’)
The main question: django-filter the best option?
Question 1: what’s the best option of accomplishing this overall?
Question 2: how do I add model fields to the filter based on user’s form selection?
Question 3: how do I do the filter based on user selection? (I know how to use .filter(price_lt=).exclude(price_gt=) but again how do I do it dynamically based on selection as “ANY” would mean this is not included in the query)
I had a similar case like yours (real estate project), I ended up with the following approach, you can refine this to your needs...I removed select_related and prefetch_related models for easier reading
properties/forms.py:
class SearchPropertyForm(forms.Form):
property_type = forms.ModelChoiceField(label=_("Property Type"), queryset=HouseType.objects.all(),widget=forms.Select(attrs={'class':'form-control input-sm'}))
location = forms.ModelChoiceField(label=_('Location'), queryset=HouseLocation.objects.all(), widget=forms.Select(attrs={'class':'form-control input-sm'}))
Then in the properties/views.py
# Create a Mixin to inject the search form in our context
class SeachPropertyMixin(object):
def get_context_data(self, **kwargs):
context = super(SeachPropertyMixin, self).get_context_data(**kwargs)
context['search_property_form'] = SearchPropertyForm()
return context
In your actual view (I apply the search form as a sidebar element in my detailview only:
# Use Class Based views, saves you a great deal of repeating code...
class PropertyView(SeachPropertyMixin,DetailView):
template_name = 'properties/view.html'
context_object_name = 'house'
...
queryset = HouseModel.objects.select_related(...).prefetch_related(...).filter(flag_active=True, flag_status='a')
Finally your search result view (this is performed as GET request, since we are not altering any data in our DB, we stick to the GET method):
# Search results should return a ListView, here is how we implement it:
class PropertySearchResultView(ListView):
template_name = "properties/propertysearchresults.html"
context_object_name = 'houses'
paginate_by = 6
queryset = HouseModel.objects.select_related(...).prefetch_related(...).order_by('-sale_price').filter(flag_active=True, flag_status='a')
def get_queryset(self):
qs = super(PropertySearchResultView,self).get_queryset()
property_type = self.request.GET.get('property_type')
location = self.request.GET.get('location')
'''
Start Chaining the filters based on the input, this way if the user has not
selected a filter it wont be used.
'''
if property_type != '' and property_type is not None:
qs = qs.filter(housetype=property_type)
if location != '' and location is not None:
qs = qs.filter(location=location)
return qs
def get_context_data(self, **kwargs):
context = super(PropertySearchResultView, self).get_context_data()
'''
Add the current request to the context
'''
context['current_request'] = self.request.META['QUERY_STRING']
return context
Your solution works. I've modified it and I'm not using ModelChoiceField but the standard form.ChoiceField. The reason for that is that I wanted to add option "Any". My "if" statements look like:
if locality != 'Any Locality':
qs = qs.filter(locality=locality)
if property_type != 'Any Type':
qs = qs.filter(property_type=property_type)
if int(price_min) != 0:
qs = qs.filter(price__gte=price_min)
if int(price_max) != 0:
qs = qs.filter(price__lte=price_max)
if bedrooms != 'Any Number':
qs = qs.filter(bedrooms=bedrooms)
And so on....
This does the job, however it seems like an ugly and hacky solution to a simple problem. I would think is a common use case. I feel there should be a cleaner solution...
I've tried the django-filter. It is close to doing what I want but I couldn't add the "Any" choice and it filters inline rather than returning. It should do with some modifications.
Cheers