Django_Tables2 Dynamic Columns with Filters - django

Goal was to implement a simple View for the Users to Select columns dynamically with some added calculated info (annotations on some specific columns) and also let them filter on fields.
Thankful for any comments, since this took me quite a few hours to get it working properly I thought I would provide a short writeup for anyone looking at a similar problem :)
Used Modules/Libraries etc:
Django-Filter
Django_Tables2
Bootstrap-Select to properly display Multiple Choice Fields
Example Model which we would like to use:
class Summary(models.Model):
billing_date = models.DateField(verbose_name='Billing Date')
period = models.CharField(max_length=10, verbose_name='Period')
operator = models.CharField(max_length=50, verbose_name='Operator')
product = models.CharField(max_length=30, verbose_name='Product')
...

The filters are really straightforward, the only special case here is that the we want an empty queryset initially and some fields should be required.
"info" will hold the select columns of our "Summary" Model, "product" and "operator" are just fields in Summary.
class AdHocReportFilter(django_filters.FilterSet):
info = django_filters.MultipleChoiceFilter(choices=report_field_choices, label='Available Fields', required=True)
product = django_filters.ModelChoiceFilter(queryset=Product.objects.all(), label='Products', required=True)
operator = django_filters.CharFilter(field_name="operator", lookup_expr='contains', label='Operator')
....
def __init__(self, *args, **kwargs):
super(AdHocReportFilter, self).__init__(*args, **kwargs)
if self.data == {}:
self.queryset = self.queryset.none()
Template:
Nothing interesting to show here, you can use Bootstrap-Select to tidy up your Multi Select Fields (there are quite a few nice writeups about that available).
Make sure to put your Table into an "if" as the object may or may not exist depending on your view (if someone wants an example of the template let me know)
View:
Extract your GET requests accordingly (either as list or simple value depending on your available filters).
The actual filter itself depends on what you want the user to be able to filter, make sure to either make all necessary fields required or replace them with some standard value as the filter will not accept None Types.
"field__contains" is your friend here since it will also show values on not selected fields!
Special Case, if there can actually be "Null" Values in the DB for specific fields, move them to another filter likethe below example of the Q - query!
Fortunately "values" accepts "*list" which is a simple list of all our available columns.
The annotations are just dependant on what you want to achieve.
Call the Table Object with the added argument "user_columns" which holds our list so we can build the required Table.
#login_required
def ad_hoc_report(request):
template_name = 'non_voice/ad_hoc_report.html'
filter = AdHocReportFilter(request.GET, queryset=Summary.objects.all())
info = request.GET.getlist('info', None)
product = request.GET.get('product', '')
operator = request.GET.get('operator', '')
start_date = request.GET.get('start_date', None)
end_date = request.GET.get('end_date', None)
if operator is None:
operator = ''
result_object = Summary.objects.filter(product__contains=product, ).filter((Q(operator__contains=operator)|Q(operator__isnull=True)).values(*info).annotate(
Amount=Sum("amount"), Count=Sum("count"))
table = AdHocReportTable(data=result_object, user_columns=info)
return render(request, template_name, {'filter': filter, 'table': table})
Table:
This was the difficult part and only possible with lots and lots of reading various stack overflow comments :)
First of all define your calculated annotation columns and set the required Meta info, the '...' is a built in placeholder without knowing the column name in advance (which helps us to move our calculated columns to the end of the Table)
In the init we first check if our "self.base_columns" are consistent with what we provided and remove columns which were deselected by our user, otherwise it would still show them empty even after filtering. (Maybe there is a nicer way to do this, haven't found it yet)
In the next step add the columns selected by our user dynamically from the mentioned above "user_columns" which we passed in the views.py
class AdHocReportTable(tables.Table):
Amount = tables.Column(verbose_name='Amount')
Count = tables.Column(verbose_name='Count')
class Meta:
# '...' is a built in placeholder!
sequence = ('...', 'Amount', 'Count')
template_name = "django_tables2/bootstrap4.html"
attrs = {'class': 'table table-hover', }
# This makes it possible to pass a dynamic list of columns to the Table Object
def __init__(self, data, user_columns, *args, **kwargs):
if user_columns:
calulated_columns = ['Amount', 'Count']
# Removes deselected columns from the table (otherwise they are shown empty)
for key, val in self.base_columns.items():
if key not in user_columns and key not in calulated_columns:
del self.base_columns[key]
# Add the Selected Columns dynamically to the Table
for col in user_columns:
self.base_columns[col] = tables.Column(verbose_name=col)
super(AdHocReportTable, self).__init__(data, user_columns, *args, **kwargs)

Related

How to filter in django with empty fields when using ChoiceField

I have a programme where users should be able to filter different types of technologies by their attributes. My question is, how would I filter the technologies when there's potential conflicts and empty values in the parameters I use to filter?
Forms.py:
class FilterDataForm(forms.ModelForm):
ASSESSMENT = (('', ''),('Yes', 'Yes'),('No', 'No'),)
q01_suitability_for_task_x = forms.ChoiceField(label='Is the technology suitable for x?',
choices=ASSESSMENT, help_text='Please select yes or no', required=False,)
q02_suitability_for_environment_y = forms.ChoiceField(label='Is the technology suitable for environment Y?',
choices=ASSESSMENT, help_text='Please select yes or no', required=False)
There are many fields in my model like the ones above.
views.py
class TechListView(ListView):
model = MiningTech
def get_queryset(self):
q1 = self.request.GET.get('q01_suitability_for_task_x', '')
q2 = self.request.GET.get('q02_suitability_for_environment_y', '')
object_list = MiningTech.objects.filter(q01_suitability_for_task_x=q1).filter(
q02_suitability_for_environment_y=q2)
return object_list
The difficulty is that not all technology db entries will have data. So in my current setup there's times where I will filter out objects that have one attribute but not another.
For instance if my db has:
pk1: q01_suitability_for_task_x=Yes; q02_suitability_for_environment_y=Yes;
pk2: q01_suitability_for_task_x=Yes; q02_suitability_for_environment_y='';
In the form, if I don't select any value for q01_suitability_for_task_x, and select Yes for q02_suitability_for_environment_y, I get nothing back in the queryset because there are no q01_suitability_for_task_x empty fields.
Any help would be appreciated. I'm also ok with restructuring everything if need be.
The problem is that your self.request.GET.get(...) code defaults to an empty string if there is no value found, so your model .filter() is looking for matches where the string is ''.
I would restructure the first part of get_queryset() to build a dictionary that can be unpacked into your filter. If the value doesn't exist then it doesn't get added to the filter dictionary:
filters = {}
q1 = self.request.GET.get('q01_suitability_for_task_x', None)
q2 = self.request.GET.get('q02_suitability_for_environment_y', None)
if q1 is not None:
filters['q01_suitability_for_task_x'] = q1
... etc ...
object_list = MiningTech.objects.filter(**filters)
If you have a lot of q1, q2, etc. items then consider putting them in a list, looping through and inserting into the dictionary if .get(...) returns anything.
Edit: Because there are indeed a lot possible filters, the final solution looks as follows:
def get_queryset(self):
filters = {}
for key, value in self.request.GET.items():
if value != '':
filters[key] = value
object_list = Tech.objects.filter(**filters)

How to create a customized filter search function in Django?

I am trying to create a filter search bar that I can customize. For example, if I type a value into a search bar, then it will query a model and retrieve a list of instances that match the value. For example, here is a view:
class StudentListView(FilterView):
template_name = "leads/student_list.html"
context_object_name = "leads"
filterset_class = StudentFilter
def get_queryset(self):
return Lead.objects.all()
and here is my filters.py:
class
StudentFilter(django_filters.FilterSet):
class Meta:
model = Lead
fields = {
'first_name': ['icontains'],
'email': ['exact'],
}
Until now, I can only create a filter search bar that can provide a list of instances that match first_name or email(which are fields in the Lead model). However, this does now allow me to do more complicated tasks. Lets say I added time to the filter fields, and I would like to not only filter the Lead model with the time value I submitted, but also other Lead instances that have a time value that is near the one I submitted. Basically, I want something like the def form_valid() used in the views where I can query, calculate, and even alter the values submitted.
Moreover, if possible, I would like to create a filter field that is not necessarily an actual field in a model. Then, I would like to use the submitted value to do some calculations as I filter for the list of instances. If you have any questions, please ask me in the comments. Thank you.
You can do just about anything by defining a method on the filterset to map the user's input onto a queryset. Here's one I did earlier. Code much cut down ...
The filter coat_info_contains is defined as a CharFilter, but it is further parsed by the method which splits it into a set of substrings separated by commas. These substrings are then used to generate Q elements (OR logic) to match a model if the substring is contained in any of three model fields coating_1, coating_2 and coating_3
This filter is not implicitly connected to any particular model field. The connection is through the method= specification of the filter to the filterset's method, which can return absolutely any queryset on the model that can be programmed.
Hope I haven't cut out anything vital.
import django_filters as FD
class MemFilter( FD.FilterSet):
class Meta:
model = MyModel
# fields = [fieldname, ... ] # default filters created for these. Not required if all declarative.
# fields = { fieldname: [lookup_expr_1, ...], ...} # for specifying possibly multiple lookup expressions
fields = {
'ft':['gte','lte','exact'], 'mt':['gte','lte','exact'],
...
}
# declarative filters. Lots and lots of
...
coat_info_contains = FD.CharFilter( field_name='coating_1',
label='Coatings contain',
method='filter_coatings_contains'
)
...
def filter_coatings_contains( self, qs, name, value):
values = value.split(',')
qlist = []
for v in values:
qlist.append(
Q(coating_1__icontains = v) |
Q(coating_2__icontains = v) |
Q(coating_3__icontains = v) )
return qs.filter( *qlist )

Django - Custom values for inline list item field

After searching for a while, I can't seem to find any answer for this so I'm asking this question. What I want to is display some values based on a person's salary and a bracket. My models looks like this.
class Employee
salary = models.DecimalField
class Benefit
some_fields
class EmployeeBenefit
employee = models.ForeignKey
benefit = models.ForeignKey
class BenefitVersion
benefit = models.ForeignKey
class BenefitBracket
benefit_version = models.ForeignKey
from_salary = models.DecimalField
to_salary = models.DecimalField
the_fields_I_want_to_display
As you can see it's quite deep, there would be a bunch of querying to do to get to the fields I want.
I basically need to get the employee's salary(which is easy because this is gonna be inside EmployeeAdmin) then get the current benefit of the EmployeeBenefit list item, then based on the benefit and the employee's salary, get the bracket and then display some of it's fields on the inline.
I want to display the the_fields_I_want_to_display on the admin.TabularInline for EmployeeBenefit inside my EmployeeAdmin. I was testing using a forms.ModelForm in the inline and modifying it's contents using get_form based on this answer but django is not calling get_form. I also previously tried using calculated_fields but it's not being rendered as well.
I'm using django 3.1.7 btw
Edit:
I found this and try it out right now. I think it has potential but unfortunately the obj that get_formset gives is Employee so I still need to find a way to get the right EmployeeBenefit for the list item
After some more searching, I already figured out a way to do this. Based on this SO answer, I got an idea on how I should receive the data I passed to the formset using get_formset.
So, in my get_formset, I did something like this.
def get_formset(self, request, obj=None, **kwargs):
formset = super(Inline, self).get_formset(request, obj, **kwargs)
formset.request = request
benefit_details_dict_arr = []
emp_contribs = models.EmployeeBenefit.objects.filter(employee=obj)
#because what I'm getting here is Employee and not a EmployeeBenefit, I have to query it first
for contrib in emp_contribs:
bracket = contrib.get_benefit_bracket(datetime.now())
benefit_details_dict_arr.append({
"key": bracket.val,
"key2": bracket.val2,
})
formset.benefit_details = benefit_details_dict_arr
return formset
Then on the formset, I receive the data this way.
class EmpBenefitFormSet(forms.models.BaseInlineFormSet):
model = models.EmployeeBenefit
def __init__(self, *args, **kwargs):
super(EmpBenefitFormSet, self).__init__(*args, **kwargs)
cntr = 0
for form in self.forms:
init_data = self.benefit_details[cntr]
for field in form.fields:
if field in init_data:
form.initial[field] = init_data[field]
form.fields[field].required = False
form.fields[field].disabled = True
cntr += 1
Also, if you're gonna use this way of receiving the data, each field you want to inflate should be declared on the fields of the inline. This should work if the fields you want to set an initial value are actual fields of the model. You might need to tweak this a bit to work for custom fields

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.

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