I've got a question - can Django Admin interface be customized in such a way, that it presents only those filter options that occur in the subset of data matching currently selected filters?
Say I have a db of three objects:
a.Foo = "One"
a.Bar = "Drink"
b.Foo = "One"
b.Bar = "Shot"
c.Foo = "Two"
c.Bar = "Shot"
And a django admin interface with filters on 'Foo' and 'Bar'. I want the following behavior:
If no filters are chosen, 'Foo' lists "One","Two"; 'Bar' lists "Drink", "Shot"
If 'Foo' filter is set to "One", 'Bar' lists both "Drink" and "Shot"
If 'Foo' filter is set to "Two", 'Bar' lists only "Shot"
If 'Bar' filter is set to "Shot", 'Foo' lists both "One" and "Two"
If 'Bar' filter is set to "Drink", 'Foo' lists only "One"
Cheers!
To be more specific - after reading some docs:
from django.contrib.admin import SimpleListFilter
class SomeFilter(SimpleListFilter):
title = "Foo"
parameter_name="Some"
def lookups(self, request, model_admin):
qs = model_admin.queryset(request)
print qs.query
return (('Foo', 'Bar'))
def queryset(self, request, queryset):
if (self.value()):
return queryset.filter(Some=self.value())
else:
return queryset
What it does, however, is gets the 'queryset' as it would've been without other filters. How can I pass it through other filters?
I could theoretically parse the request and filter manually - but there surely needs to be a way to pipe all filters.
This kind of dynamic filtering really looks like faceting. While you may be able to achieve this result with a standard queryset, this will probably not be optimal. You may have more chance using a dedicated tool, like Solr.
Haystack has also a doc page on faceting.
in case anyone needs a lightweight solution, this one does a Country filter that is hidden when no Continent is selected in the filter and only offers those countries that exist in the selected Continent:
from django.utils.translation import ugettext_lazy as _
class ContinentCountryListFilter(admin.SimpleListFilter):
# Human-readable title which will be displayed in the
# right admin sidebar just above the filter options.
title = _('country')
# Parameter for the filter that will be used in the URL query.
parameter_name = 'country__iso_code__exact'
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.
"""
continent = request.GET.get('country__continent__iso_code__exact', '')
if continent:
countries = models.Country.objects.filter(continent__iso_code__exact=continent)
else:
countries = models.Country.objects.none()
return countries.values_list('iso_code', 'name')
def queryset(self, request, queryset):
"""
Returns the filtered queryset based on the value
provided in the query string and retrievable via
`self.value()`.
"""
continent = request.GET.get('country__continent__iso_code__exact', '')
# only apply filter if continent is set and country exists in continent
if continent and models.Country.objects.filter(continent__iso_code__exact=continent, iso_code__exact=self.value()).count():
return queryset.filter(country__iso_code__exact=self.value())
return queryset
and then apply with:
list_filter = ('country__continent', ContinentCountryListFilter, )
Related
I have a variable 'cptCodeTBX' which is not present as fields in django models. I need to apply filter on 'cptCodeTBX' variable. Something roughly equivalent to
cptCodeTBX = '00622'
select * from cpt where cpt.code like cptCodeTBX or cptCodeTBX is != ''
In dot net entity framework we could do it by
b = cptContext.CPTs.AsNoTracking().Where(
a =>
(String.IsNullOrEmpty(cptCodeTBX) || a.Code.StartsWith(cptCodeTBX))
This may not be the most performant solution, but I was able to get it working.
Step 1: Read the Django Filter docs.
https://django-filter.readthedocs.io/en/stable/index.html
Step 2: Add a property to your Django model named cptCodeTBX.
from django.db import models
class MyModel(models.Model):
field = models.CharField(max_length=60)
#property
def cptCodeTBX(self):
"""
Does all the code tbx cpt stuff, can do whatever you want.
"""
cptCodeTBX = 2323 #whatever value you want
return cptCodeTBX
Step 3: Add a Django Filter.
import django_filters
class CptCodeTBXFilter(django_filters.FilterSet):
"""
Filter that will do all the magic, value comes from url query params.
Replace filters.NumberFilter with any filter you want like
filters.RangeFilter.
"""
cptCodeTBX = django_filters.NumberFilter(
field_name="cptCodeTBX",
method="filter_cptCodeTBX",
label="Cpt Code TBX",
)
def filter_cptCodeTBX(self, queryset, name, value):
objects_ids = [
obj.pk for obj in MyModel.objects.all() if obj.cptCodeTBX == value
]
if objects_ids:
queryset = MyModel.objects.filter(pk__in=objects_ids)
else:
queryset = MyModel.objects.none()
return queryset
Step 4: Pass the value through the url as a query parameter.
http://example.com/?cptCodeTBX=00622
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)
I'm overriding Django Admin's list_filter (to customize the filter that shows on the right side on the django admin UI for a listview). The following code works, but isn't optimized: it increases SQL queries by "number of product categories".
(The parts to focus on, in the following code sample are, qs.values_list('product_category', flat=True) which only returns an id (int), so I've to use ProductCategory.objects.get(id=i).)
Wondering if this can be simplified?
(E.g. data: Suppose the product categories are "baked" "fried" "raw" etc., and the Items are "bread" "fish fry" "cake". So when the Item list is displayed in Django Admin, all product categories will show on the 'Filter By' column on the right side of the UI.)
from django.utils.translation import ugettext_lazy as _
from django.contrib.admin import SimpleListFilter
from product_category.model import ProductCategory
class ProductCategoryFilter(SimpleListFilter):
title = _('ProductCategory')
parameter_name = 'product_category'
def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request)
ordered_filter_obj_list = []
# TODO: Works, but increases SQL queries by "number of product categories"
for i in (
qs.values_list("product_category", flat=True)
.distinct()
.order_by("product_category")
):
cat = ProductCategory.objects.get(id=i)
ordered_filter_obj_list.append((i, cat))
return ordered_filter_obj_list
def queryset(self, request, queryset):
if self.value():
return queryset.filter(product_category__exact=self.value())
# P.S. Above filter is used in another class like so
class ItemAdmin(admin.ModelAdmin):
list_filter = (ProductCategoryFilter,)
Probably you are looking for select_related, I do not know your exact models structure, but you may use it as follow:
cats = set()
for p in Product.objects.all().select_related('category'):
# Without select_related(), this would make a database query for each
# loop iteration in order to fetch the related categories for each product.
cats.add(p.category)
I am Assuming there is some relation between your Product and ProductCategory models. Hope this help.
Hah, phrasing the question makes it clear in your own head! Found an answer mins after posting this:
(Instead of doing an objects.get() inside the for loop, we can do objects.all() (which is a single SQL Query) and fill up a temporary dictionary. Then use this temp dict to find the associated string value.)
def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request)
category_list = {}
for x in ProductCategory.objects.all():
category_list[x.id] = str(x)
ordered_filter_obj_list = []
for i in (
qs.values_list("product_category", flat=True)
.distinct().order_by("product_category")
):
ordered_filter_obj_list.append((i, category_list[i]))
return ordered_filter_obj_list
First parameter on the tuple list is the value of the lookup, and the second is just the name for display. This can be done in a single SQL query, or via the Django ORM:
def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request).select_related('product_category')
values = qs.values('product_category_id', 'product_category__name') #assuming ProductCategory has an attribute 'name'
unique_categories = values.distinct('product_category_id', 'product_category__name')
categories = []
for c in unique_categories:
categories.append((c['product_category_id'], c['product_category__name']))
return categories
I am developing an application using django where the UI needs to be updated when user interacts with it. For instance I have a Drop down field where the user selects a drink and submits it then based on that a dropdown with the places that drink is available, price and quantity at each place needs to be displayed. The user will then further submit the form for second process.
From my understanding the Forms in django are pre-defined and I am not able to think of a way using which I could achieve this.
What I could come up was defining a regular form class
class dform(forms.Form):
SOURCES_CHOICES = (
(A, 'A'),
(E, 'E'),
)
drink = forms.ChoiceField(choices = SOURCES_CHOICES)
location = forms.ChoiceField(choices = **GET THIS FROM DATABASE**)
quantity = forms.ChoiceField(choices = **GET THIS FROM DATABASE**)
.
.
.
My view is like,
def getdrink():
if request.method == 'POST':
#code for handling form
drink = dform.cleaned_data['drink']
#code to get values from database
I have no idea how to generate or populate or append the values i get from the database to the choicefield in my form. I did try looking up on SO but none of the solutions here explained properly how to do it. Also, due to certain requirements I am not using the models. So my database is not at all related to the models.
I am at a total loss Please help me out
class MyForm(forms.Form):
my_choice_field = forms.ChoiceField(choices=MY_CHOICES)
So if you want the values to be dynamic(or dependent of some logic) you can simply modify your code to something like this:
either
def get_my_choices():
# you place some logic here
return choices_list
class MyForm(forms.Form):
my_choice_field = forms.ChoiceField(choices=get_my_choices())
or
User_list = [ #place logic here]
class MyForm(forms.Form):
my_choice_field = forms.ChoiceField(choices=get_my_choices())
but once database value is updated, new data value will be popoulated only on restart of server.
So write a function like this in forms:
class MyForm(forms.Form):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['my_choice_field'] = forms.ChoiceField( choices=get_my_choices() )
or in place of the get_my_choices u can ad the USER_LIST too.
If you have models for location and quantity, a ModelChoiceField should work:
class dform(forms.Form):
location = forms.ModelChoiceField(queryset = Location.objects.all())
Otherwise, you'll need to query the database directly, for example:
class dform(forms.Form):
location = forms.ChoiceField(choices = get_location_choices())
# elsewhere
from django.db import connection
def get_location_choices():
cursor = connection.cursor()
cursor.execute("select location_id, name from location_table")
return cursor.fetchall()
The SQL query to use here depends on your database engine and table schema.
I think that, based on my understanding of your question, the best solution would be to include JSON objects with your form and load these using jQuery instead of submitting the form over and over. Included in your form, you should add something like:
class MyForm(forms.Form):
CHOICE_DICT = {
'choice_1': [
'option_1',
'option_2',
],
etc...
Then you should include form.CHOICE_DICT in your context, load that with jQuery, and render it depending on changes to other fields.
I have a boolean field on my model that represents whether someone has canceled their membership or not. I am trying to create a custom SimpleListFilter that allows this field to be filtered on.
However, I really want to show only those who are not canceled by default. Is there someway to select the "No" option by default? This is my filter so far:
class CanceledFilter(SimpleListFilter):
title = 'Canceled'
# Parameter for the filter that will be used in the URL query.
parameter_name = 'canceled'
def lookups(self, request, model_admin):
return (
(True, 'Yes'),
(False, 'No'),
)
def queryset(self, request, queryset):
if self.value() is True or self.value() is None:
return queryset.filter(canceled=True)
if self.value() is False:
return queryset.filter(canceled=False)
EDIT:
I should have been a bit clearer. I am specifically trying to do this in the Admin interface. When I add the above filter as a list_filter in admin. I get a filter on the side of the admin page with 3 choices: All, Yes and No.
I would like the "No" choice or none of the choices to be set by default. Instead the "All" choice is always set by default. Is there some none hacky way to set the default filter choice or something like that.
Basiclly in Admin when they view the Members, I only want to show the active (not canceled) by default. If they click "All" or "Yes" then I want to show the canceled ones.
Update:
Note this is the same as question Default filter in Django admin, but I that question is now 6 years old. The accepted answer is marked as requiring Django 1.4. I am not sure if that answer will still work with newer Django versions or is still the best answer.
Given the age of the answers on the other question, I am not sure how we should proceed. I don't think there is any way to merge the two.
Had to do the same and stumbled upon your question. This is how I fixed it in my code (adapted to your example):
class CanceledFilter(SimpleListFilter):
title = 'Canceled'
# Parameter for the filter that will be used in the URL query.
parameter_name = 'canceled'
def lookups(self, request, model_admin):
return (
(2, 'All'),
(1, 'Yes'),
(0, 'No'),
)
def queryset(self, request, queryset):
if self.value() is None:
self.used_parameters[self.parameter_name] = 0
else:
self.used_parameters[self.parameter_name] = int(self.value())
if self.value() == 2:
return queryset
return queryset.filter(cancelled=self.value())
Some explanation is required. The querystring is just part of the URL, and exactly what the name implies: a query string. Your values come in as strings, not as booleans or integers. So when you call self.value(), it returns a string.
If you examine the URL you get when you click on the Yes/No, when not using a custom list filter, you'll see it encodes it as 1/0, not True/False. I went with the same scheme.
For completeness and our future readers, I also added 2 for All. Without verifying, I assume that was None before. But None is also used when nothing is selected, which defaults to All. Except, in our case it needs to default to False, so I had to pick a different value. If you don't need the All option, just remove the final if-block in the queryset method, and the first tuple in the lookups method.
With that out of the way, how does it work? The trick is in realising that self.value() just returns:
self.used_parameters.get(self.parameter_name, None)
which is either a string, or None, depending on whether the key is found in the dictionary or not. So that's the central idea: we make sure it contains integers and not strings, so that self.value() can be used in the call to queryset.filter(). Special treatment for the value for All, which is 2: in this case, just return queryset rather than a filtered queryset. Another special value is None, which means there is no key parameter_name in the dictionary. In that case, we create one with value 0, so that False becomes the default value.
Note: your logic was incorrect there; you want the non-cancelled by default, but you treat None the same as True. My version corrects this.
ps: yes, you could check for 'True' and 'False' rather than True and False in your querystring method, but then you'd notice the correct selection would not be highlighted because the first elements in your tuple don't match up (you're comparing strings to booleans then). I tried making the first elements in the tuples strings too, but then I'd have to do string comparison or eval to match up 'True' to True, which is kind of ugly/unsafe. So best stick to integers, like in my example.
If anyone is still interested in a solution for this, I used a different and IMHO much cleaner approach. As I'm fine with a default choice and the handling of it, I decided I just want to rename the default display label. This is IMHO much cleaner and you don't need any "hacks" to handle the default value.
class CompleteFilter(admin.SimpleListFilter):
'''
Model admin filter to filter orders for their completion state.
'''
title = _('completion')
parameter_name = 'complete'
def choices(self, changelist):
'''
Return the available choices, while setting a new default.
:return: Available choices
:rtype: list
'''
choices = list(super().choices(changelist))
choices[0]['display'] = _('Only complete')
return choices
def lookups(self, request, model_admin):
'''
Return the optionally available lookup items.
:param django.http.HttpRequest request: The Django request instance
:param django.contrib.admin.ModelAdmin model_admin: The model admin instance
:return: Optional lookup states
:rtype: tuple
'''
return (
('incomplete', _('Only incomplete')),
('all', _('All')),
)
def queryset(self, request, queryset):
'''
Filter the retreived queryset.
:param django.http.HttpRequest request: The Django request instance
:param django.db.models.query.QuerySet: The Django database query set
:return: The filtered queryset
:rtype: django.db.models.query.QuerySet
'''
value = self.value()
if value is None:
return queryset.filter(state__complete=True)
elif value == 'incomplete':
return queryset.filter(state__complete=False)
return queryset
In the choices() method, I just rename the display label from All to Only complete. Thus, the new default (which has a value of None is now renamed).
Then I've added all additional lookups as usual in the lookups() method. Because I still want an All choice, I add it again. However, you can also skip that part if you don't need it.
That's basically it! However, if you want to display the All choice on top again, you might want to reorder the choices list in the choices() method before returning it. For example:
# Reorder choices so that our custom "All" choice is on top again.
return [choices[2], choices[0], choices[1]]
Look at the section called "Adding Extra Manager Methods" in the link below:
http://www.djangobook.com/en/2.0/chapter10.html
You can add an additional models.Manager to your model to only return people that have not cancelled their membership. A rough implementation of the additional models.Manager would look like this:
class MemberManager(models.Manager):
def get_query_set(self):
return super(MemberManager, self).get_query_set().filter(membership=True)
class Customer(models.Model):
# fields in your model
membership = BooleanField() # here you can set to default=True or default=False for when they sign up inside the brackets
objects = models.Manager # default Manager
members = MemberManager() # Manager to filter for members only
Anytime you need to get a list of you current members only, you would then just call:
Customer.members.all()