I read on several places that it is not possible to filter Django querysets using properties because Django ORM would have no idea how to convert those into SQL.
However, once the data are fetched and loaded into memory, it shall be possible to filter them in Python using those properties.
And my question: is there any library that allows the querysets to be filtered by properties in memory? And if not, how exactly must the querysets be tampered with so that this becomes possible? And how to include django-filter into this?
Have you got difficult property or not?
If not you can rewrite it to queryset like this:
from django.db import models
class UserQueryset(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(
has_profile=models.Exists(Profile.objects.filter(user_id=models.OuterRef('id')))
)
class User(models.Model):
objects = UserQueryset
class Profile(models.Model):
user = models.OneToOneField(User, related_name='profile')
# When you want to filter by has profile just use it like has field has profile
user_with_profiles = User.objects.filter(has_profile=True)
Maybe it is not what you want, but it can help you in some cases
django-filter wants and assumes that you are using querysets. Once you take a queryset and change it into a list, then whatever is downstream needs to be able to handle just a list or just iterate through the list, which is no longer a queryset.
If you have a django_filters.FilterSet like:
class FooFilterset(django_filters.FilterSet):
bar = django_filters.Filter('updated', lookup_expr='exact')
my_property_filter = MyPropertyFilter('property')
class Meta:
model = Foo
fields = ('bar', 'my_property_filter')
then you can write MyPropertyFilter like:
class MyPropertyFilter(django_filters.Filter):
def filter(self, qs, value):
return [row for row in qs if row.baz == value]
At this point, anything downstream of MyProperteyFilter will have a list.
Note: I believe the order of fields should have your custom filter, MyPropertyFilter last, because then it will always be processed after the normal queryset filters.
So, you have just broken the "queryset" API, for certain values of broken. At this point, you'll have to work through the errors of whatever is downstream. If whatever is after the FilterSet requires a .count member, you can change MyPropertyFilter like:
class MyPropertyFilter(django_filters.Filter):
def filter(self, qs, value):
result = [row for row in qs if row.baz == value]
result.count = len(result)
return result
You're in uncharted territory, and you'll have to hack your way through.
Anyways, I've done this before and it isn't horrible. Just take the errors as they come.
Since filtering by non-field attributes such as property inevitably converts the QuerySet to list (or similar), I like to postpone it and do the filtering on object_list in get_context_data method. To keep the filtering logic inside the filterset class, I use a simple trick. I've defined a decorator
def attr_filter(func):
def wrapper(self, queryset, name, value, force=False, *args, **kwargs):
if force:
return func(self, queryset, name, value, *args, **kwargs)
else:
return queryset
return wrapper
which is used on django-filter non-field filtering methods. Thanks to this decorator, the filtering basically does nothing (or skips) the non-field filtering methods (because of force=False default value).
Next, I defined a Mixin to be used in the view class.
class FilterByAttrsMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
filtered_list = self.filter_qs_by_attributes(self.object_list, self.filterset)
context.update({
'object_list': filtered_list,
})
return context
def filter_qs_by_attributes(self, queryset, filterset_instance):
if hasattr(filterset_instance.form, 'cleaned_data'):
for field_name in filter_instance.filters:
method_name = f'attr_filter_{field_name}'
if hasattr(filterset_instance, method_name):
value = filterset_instance.form.cleaned_data[field_name]
if value:
queryset = getattr(filterset_instance, filter_method_name)(queryset, field_name, value, force=True)
return queryset
It basically just returns to your filterset and runs all methods called attr_filter_<field_name>, this time with force=True.
In summary, you need to:
Inherit the FilterByAttrsMixin in your view class
call your filtering method attr_filter_<field_name>
use attr_filter decorator on the filtering method
Simple example (given that I have model called MyModel with property called is_static that I want to filter by:
model:
class MyModel(models.Model):
...
#property
def is_static(self):
...
view:
class MyFilterView(FilterByAttrsMixin, django_filters.views.FilterView):
...
filterset_class = MyFiltersetClass
...
filter:
class MyFiltersetClass(django_filters.FilterSet):
is_static = django_filters.BooleanFilter(
method='attr_filter_is_static',
)
class Meta:
model = MyModel
fields = [...]
#attr_filter
def attr_filter_is_static(self, queryset, name, value):
return [instance for instance in queryset if instance.is_static]
Take a look at django-property-filter package. This is an extension to django-filter and provides functionality to filter querysets by class properties.
Short example from the documentation:
from django_property_filter import PropertyNumberFilter, PropertyFilterSet
class BookFilterSet(PropertyFilterSet):
prop_number = PropertyNumberFilter(field_name='discounted_price', lookup_expr='gte')
class Meta:
model = NumberClass
fields = ['prop_number']
Related
I have a custom queryset on a model manager:
class TenantManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(myfield=myvalue)
class TenantModel(TenantModelMixin, models.Model):
objects = TenantManager()
class Meta:
abstract = True
I use the abstract TenantModel as a mixin with another model to apply the TenantManager. E.g.
class MyModel(TenantModel):
This works as expected, applying the TenantManager filter every time MyModel.objects.all() is called when inside a view.
However, when I create a ModelForm with the model, the filter is not applied and all results (without the filter are returned. For example:
class AddPersonForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('person', )
Why is this and how to I ensure the ModelManager is applied to the queryset in ModelForm?
Edit
#Willem suggests the reason is forms use ._base_manager and not .objects (although I can not find this in the Django source code), however the docs say not to filter this kind of manager, so how does one filter form queries?
Don’t filter away any results in this type of manager subclass
This
manager is used to access objects that are related to from some other
model. In those situations, Django has to be able to see all the
objects for the model it is fetching, so that anything which is
referred to can be retrieved.
If you override the get_queryset() method and filter out any rows,
Django will return incorrect results. Don’t do that. A manager that
filters results in get_queryset() is not appropriate for use as a base
manager.
You can do it in two ways:
First: When creating the form instance, add the queryset for the desired field.
person_form = AddPersonForm()
person_form.fields["myfield"].queryset = TenantModel.objects.filter(myfield="myvalue")
Second: Override the field's queryset in the AddPersonForm itself.
class AddPersonForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('person', )
def __init__(self, *args, **kwargs):
super(AddPersonForm, self).__init__(*args, **kwargs)
self.fields['myfield'].queryset = TenantModel.objects.filter(myfield="myvalue")
I'm not sure why your code doesn't properly works. Probably you haven't reload django app. You could load queryset in __init__ of your form class
class AddPersonForm(forms.ModelForm):
person = forms.ModelMultipleChoiceField(queryset=None)
class Meta:
model = MyOtherModel
fields = ('person', )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['person'].queryset = MyModel.objects.all()
Say we have a column like:
num_member = tables.Column(accessor = 'members.count', verbose_name = 'number of members' )
When I tried to sort this in the template, it raises:
Field Error: Cannot resolve keyword u'count' into field
I read the document and it says we can use order_by by passing in some sort of accessor, but how exactly do we do this please?
For function like Model's property method, you can access it directly using accessor. For example:
Class MyModel(models.Model):
data= models.CharField(max_length=255)
#property
def print_function(self):
return 'hello world'
#Table class
class MyTable(tables.Table):
data= tables.Column(accessor='print_function')
class Meta:
model = MyModel
fields = ('data')
Using the above method, you can show different kinds of data in table using accessor:
Class SomeModel(models.Model):
some_data= models.CharField(max_length=255)
data= models.ManyToManyField(MyModel)
#property
def count_function(self):
some_data= self.data.objects.count() #returns count of the objects
return some_data
#Table class
class SomeTable(tables.Table):
data= tables.Column(accessor='count_function')
class Meta:
model = SomeModel
fields = ('data')
And accessor can be used for directly accessing related foreignkey model's field value like:
Class SomeModel(models.Model):
somedata= models.ForeignKey(MyModel)
#Table class
class MyTable(tables.Table):
data= tables.Column(accessor='somedata.data')
class Meta:
model = SomeModel
fields = ('data')
EDIT
Lets give an example of how order_by can be used:
#Class Based view, I am subclassing ListView and SingleTableView here for example
class MyView(ListView, SingleTableView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['table'].order_by = '-last_updated' #last_updated is a datetimefield in model
return context
In the above code, what I have done is that, I am changing the order of the table data in context which will later be rendered in template.
Fairly old question, however, I have been confronted with the same problem today: I couldn't order my tables if the accessor was a property (or 0-argument-method) and not a model field.
After not finding anything in the docs and inspecting the source code, it turned out that tables2 will pass the ordering to the database if its data is a QuerySet, but otherwise it will do a Python list sort with an appropriate key:
# django_tables2/tables.py -> class TableData
def order_by(self, aliases):
# ...
if hasattr(self, "queryset"):
translate = lambda accessor: accessor.replace(Accessor.SEPARATOR, QUERYSET_ACCESSOR_SEPARATOR)
if accessors:
self.queryset = self.queryset.order_by(*(translate(a) for a in accessors))
else:
self.list.sort(key=OrderByTuple(accessors).key)
I assume that this can not be trivially solved by using a try-except instead of the if-else because an exception would only be raised once the queryset is evaluated which only happens later.
Solution: whenever your sort-parameter is not a model field, turn the QuerySet into a list before handing it to the table. For many cases in django, this will be as simple as overriding get_queryset:
def get_queryset(self):
qs = super(ViewName, self).get_queryset()
return list(qs)
This should work best if your accessor is a cached_property on the model of your table, e.g.:
from django.utils.functional import cached_property
#cached_property
def member_count(self):
# do the heavy stuff here in the model
return whatever
Then, in the table:
num_member = tables.Column(
accessor='members_count',
verbose_name='number of members'
)
Given a serializer with a reference to a custom serializer:
class IndustryIdeaSerializer(serializers.ModelSerializer):
sub_industry = IndustrySerializer(many=False, read_only=True)
class Meta:
model = myModels.IdeaIndustry
fields = (
'id'
, 'sub_industry'
)
I am unable to save changes to this class when I post JSON like { sub_industry: 12 } or { sub_industry_id: 12 }
It does return the right JSON for displaying the data, and I wouldn't change it from that perspective. However changing it to:
class IndustryIdeaSerializer(serializers.ModelSerializer):
class Meta:
model = myModels.IdeaIndustry
fields = (
'id'
, 'sub_industry'
)
Gives me the save action (can persist with the simple JSON) I want BUT not the read action (doesn't return all the data associated with that foreign key)!
First am I missing something obvious? Is there a pattern to deal with behavior I am after - namely read and return the deep tree, but persist with just the Id's
This is for DRF 3.0. I just whipped this up this afternoon, I will follow up if I encounter any unforeseen problems (likewise, let me know if you spot anything wrong! I am fairly new to DRF)
class EnhancedPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
'''
This custom field extends the PrimaryKeyRelatedField
It overrides to_representation (which generates the data to be
serialized) to use a given serializer.
This allows other serializers to show nested data about a related
field, while still allowing the client to set relations by simply
passing an id.
To initialize, pass the queryset and serializer arguments.
The serializer argument should be a Serializer class.
If the serializer provides Meta.model (such as a ModelSerializer),
and you wish to use the queryset provided by that serializer, you may
omit the queryset argument.
e.g.
# without queryset
child_object = EnhancedPrimaryKeyRelatedField(
serializer=ChildObjectSerializer
)
# with queryset
child_object = EnhancedPrimaryKeyRelatedField(
queryset=models.ChildObject.objects.all(),
serializer=SomeSpecializedSerializer
)
'''
def __init__(self, *args, **kwargs):
assert 'serializer' in kwargs
self.serializer = kwargs['serializer']
del kwargs['serializer']
if 'queryset' not in kwargs:
# Catch any programmer errors
assert 'Meta' in self.serializer.__dict__
assert 'model' in self.serializer.Meta.__dict__
kwargs['queryset'] = self.serializer.Meta.model.objects.all()
super(serializers.PrimaryKeyRelatedField, self).__init__(*args, **kwargs)
def to_representation(self, data):
if hasattr(data.pk, 'all'): # are we dealing with a collection?
return self.serializer(data.pk.all(), many=True).data
elif hasattr(data, 'pk') and data.pk:
return self.serializer(self.queryset.get(pk=data.pk)).data
else:
return data.pk
There's nothing built in that handles this explicitly, but it's now come up a couple of times recently (e.g. here so perhaps we need to make it easier.
The work-around is to subclass PrimaryKeyRelatedField, which will handle setting the relation and override to_native to provide the full serialisation you're after.
I hope that helps.
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]
I'm using Django 1.3's class based generic view to display a list of images, but I want to add a filter that enables the user to narrow down the displayed results.
My current approach works, but feels quite hackish:
class ImageFilterForm(ModelForm):
class Meta:
model = Image
class ImageListView(ListView):
model = Image
def get_queryset(self):
qs = Image.objects.select_related()
for item in self.request.GET:
key, value = item, self.request.GET.getlist(item)
# ... Filtering here ...
return qs
def get_context_data(self, **kwargs):
context = super(ImageListView, self).get_context_data(**kwargs)
context['filter_form'] = ImageFilterForm(self.request.GET)
return context
Are there better means to add custom filtering to a generic view?
I use the same approach, but generic, using a mixin:
class FilterMixin(object):
def get_queryset_filters(self):
filters = {}
for item in self.allowed_filters:
if item in self.request.GET:
filters[self.allowed_filters[item]] = self.request.GET[item]
return filters
def get_queryset(self):
return super(FilterMixin, self).get_queryset()\
.filter(**self.get_queryset_filters())
class ImageListView(FilterMixin, ListView):
allowed_filters = {
'name': 'name',
'tag': 'tag__name',
}
# no need to override get_queryset
This allows to specify a list of accepted filters, and they don't need to correspond to the actual .filter() keywords. You can then expand it to support more complex filtering (split by comma when doing an __in or __range filter is an easy example)
Take a look at django-filter it easy solution for filtering data in view