OR logic with Django-filter - python

I'm using Django-filter to allow the user to filter a database based on multiple choices in two fields. The filterset.py looks like this:
class TapFilter(django_filters.FilterSet):
bar__region = django_filters.MultipleChoiceFilter(choices=CHOICES, label="Regions:", widget=forms.CheckboxSelectMultiple,help_text="")
bar = django_filters.ModelMultipleChoiceFilter(queryset=Bar.objects.all(), label="Bars:", widget=forms.CheckboxSelectMultiple,help_text="")
However, this functions as an AND between the two lists. I need OR instead. That is, I need to show anything matching the selection in either category.
I have seen similar questions using normal filters, but I would prefer to keep using django-filter if possible.
The website in question is here: http://bestap.pythonanywhere.com/
Update: I've put this in my filtersets.py, but am clearly not doing things right...
class TapFilter(django_filters.FilterSet):
bar__region = django_filters.MultipleChoiceFilter(choices=CHOICES, label="Regions:", widget=forms.CheckboxSelectMultiple,help_text="")
bar = django_filters.ModelMultipleChoiceFilter(queryset=Bar.objects.all(), label="Bars:", widget=forms.CheckboxSelectMultiple,help_text="")
def qs(self):
base_qs = Bar.objects.all()
qs = Bar.objects.none()
for name, filter_ in six.iteritems(self.filters):
value = self.form.cleaned_data[name]
qs = qs | filter_.filter(base_qs, value)
return qs
This gives me the error 'function' object has no attribute 'count'.

You'll need to override qs on your TapFilter FilterSet subclass.
The base implementation is not that complicated; the essence of it loops over the filters applying them to the queryset.
Simplified:
for name, filter_ in six.iteritems(self.filters):
value = self.form.cleaned_data[name]
qs = filter_.filter(qs, value)
You need the union of the filters' QuerySets, which you can get because QuerySet implements __or__, so (again simplified) you'll need something like:
base_qs = Bar.objects.all()
qs = Bar.objects.none()
for name, filter_ in six.iteritems(self.filters):
value = self.form.cleaned_data[name]
qs = qs | filter_.filter(base_qs, value)
Hopefully that gets you started.

Related

How to show only one object in django admin list view?

Problem
I wish to show only the last row of a QuerySet based on the ModelAdmin's ordering criteria. I have tried a couple of methods, but none has worked for me.
Model:
class DefaultConfig(models.Model):
created_at = models.DateTimeField()
...
Attempt 1:
I tried overriding the ModelAdmin's get_queryset method and slicing super's result, but I came up with some issues.
I.E:
class DefaultConfigAdmin(models.ModelAdmin):
model = Config
ordering = ('-created_at',)
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs[<slice>]
I tried the following values for [<slice>]s:
[-1:]: raised an Exception because negative slicing is not supported
[:1]: raised AssertionError: Cannot reorder a query once a slice has been taken.
Attempt 2:
I tried obtaining the max value for created_at and then filtering for records with that value. I.E:
class DefaultConfigAdmin(models.ModelAdmin):
model = Config
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(max_created_at=Max('created_at')).filter(created_at=F('max_created_at'))
But silly me, that works at row level, so it will only return aggregates over the row itself.
Further attempts (TBD):
Perhaps the answer lies in SubQuerys or Windowing and ranking.
Is there a more straight forward way to achieve this though?
Did you try to set list_per_page = 1?
class DefaultConfigAdmin(models.ModelAdmin):
model = Config
ordering = ('-created_at',)
list_per_page = 1
Technically this will still return all Config objects, but only one per page and the latest one will be on the first page.
Another solution (similar to your "Attempt 2"), which involves an extra query, is to manually get hold of the latest created_at timestamp and then use it for filtering.
class DefaultConfigAdmin(models.ModelAdmin):
model = Config
def get_queryset(self, request):
qs = super().get_queryset(request)
latest_config = qs.order_by('created_at').last()
return qs.filter(created_at=latest_config.created_at)

Django-filter 2 use #property to filter?

I've got this filter:
class SchoolFilter(django_filters.FilterSet):
class Meta:
model = School
fields = {
'name': ['icontains'],
'special_id': ['icontains'],
}
Where special_id is a #property of the School Model:
#property
def special_id(self):
type = self.type
unique_id = self.unique_id
code = self.code
if unique_id < 10:
unique_id = f'0{unique_id}'
if int(self.code) < 10:
code = f'0{self.code}'
special_id = f'{code}{type}{id}'
return special_id
I've tried to google some answers, but couldn't find anything. Right now If I use my filter like I do I only receive this error:
'Meta.fields' contains fields that are not defined on this FilterSet: special_id
How could I define the property as a field for this FilterSet? Is it even possible for me to use django-filter with a #property?
Thanks for any answer!
Update:
Figured it out. Not the prettiest solution, but ayyy
class SchoolFilter(django_filters.FilterSet):
special_id = django_filters.CharFilter(field_name="special_id", method="special_id_filter", label="Special School ID")
def special_id_filter(self, queryset, name, value):
schools_pk = []
for obj in queryset:
if obj.special_id == value:
schools_pk.append(obj.pk)
queryset = queryset.filter(pk__in=schools_pk)
return queryset
class Meta:
model = School
fields = {
'name': ['icontains'],
'special_id': ['icontains'],
}
You can't. FilterSet will only filter on actual fields, since FilterSet alters a QuerySet.
QuerySets do a database call based on the filters applied, which means you can only filter on fields actually stored in the database.
You could annotate your QuerySet to add the special_id, but an annotation like this is pretty complex to chain together.
A better way to do this would be to create a custom filter on your FilterSet, but I'm not exactly sure how to do this. If you can explain what special_id is, and exactly why you want to search it through icontains, then I could maybe point you in the right direction.
This is an implementation of a MethodFilter, which I think is similar what you want.
FilterSet operates by filtering queryset (adding where conditions to the underlying sql). Which means, FilterSet can operate only on Columns that are present in the database. Here the special_id is a computed property (It is not a column, it is calculated on the fly using other fields/columns), So it wont work.
The work around is to make special_id a normal field/column, compute the value at runtime and write to database at the time of save.

Django ORM: customize a ListView, and add more information to its queryset

I have a view to list a certain model (lets call it class A), like this:
class BaseListView(ListView, MultipleObjectMixin):
http_method_names = ['get']
order_field = None
def get_paginate_by(self, queryset):
session_manager = SessionManager(self.request.session)
return session_manager.paginate_by.get()
def get_context_data(self, **kwargs):
context = super(BaseListView, self).get_context_data(**kwargs)
session_manager = SessionManager(self.request.session)
session_manager.paginate_by.set_to(context)
return context
This view did just what was needed, till now. Now I have to compare the list of objects it retrieves with another list of objects (class B).
The objects of class A and B both have a primary key with their name.
I want to check if any of the objects from class A has the same name (primary key) as any of the objects in class B. In case there is an instance of A in B I would like to add a certain parameter or something like is_in_B=True.
I need this so that I can represent these instances of A in a different way in the template.
How could I do this?
This is what I have come up with by myself for the moment:
class AListView(BaseListView):
model = "A"
def get_queryset(self):
queryset = super(AListView, self). get_query_set()
all_objects_A = A.objects.all()
all_objects_B = B.objects.all()
# modify queryset to indicate which instances of A are present in B
# No idea how to do this
return queryset
I'm not really sure this is an appropiate approach.
Also, how am I supposed to modify the queryset returned by my class so that I can indicate which instances of class A share the same name as any of the instances of class B?
You can annotate your queryset with a conditional expression to achieve this:
from django.db.models import Case, When, Value
def get_queryset(self):
# This gives you the queryset of A objects
queryset = super(AListView, self).get_queryset()
# List of primary keys of B objects
all_objects_B = B.objects.all().values_list('pk',flat=True)
# modify queryset to indicate which instances of A are present in B
return queryset.annotate(
is_in_b=Case(When(pk__in=all_objects_B, then=Value(True)),
default=Value(False))
)
)
Your queryset objects will now have an is_in_b property.
This will work fine if your list of B objects is small. If it is large then I am not sure it is very efficient, and you may need to develop this further to see whether the check (is A in B) can be done directly in the database (possibly requiring raw SQL).

Django-REST Serializer: Queryset does not filter PrimaryKeyRelatedField results

So I have a serializer that looks like this
class BuildingsSerializer(serializers.ModelSerializer):
masterlisting_set = serializers.PrimaryKeyRelatedField(many=True,
queryset=Masterlistings.objects.all())
and it works great
serializer = BuildingsSerializer(Buildings.objects.get(pk=1))
serializer.data
produces
OrderedDict([
("masterlistings_set", [
"0a06e3d7-87b7-4526-a877-c10f54fa5bc9",
"343643ac-681f-4597-b8f5-ff7e5be65eef",
"449a3ad2-c76c-4cb8-bb86-1be72fafcf64",
])
])
but if I change the queryset in the serializer to
class BuildingsSerializer(serializers.ModelSerializer):
masterlistings_set = serializers.PrimaryKeyRelatedField(many=True, queryset=[])
I still get the same exact result back.
OrderedDict([
("masterlistings_set", [
"0a06e3d7-87b7-4526-a877-c10f54fa5bc9",
"343643ac-681f-4597-b8f5-ff7e5be65eef",
"449a3ad2-c76c-4cb8-bb86-1be72fafcf64",
])
])
Is this supposed to be happening? Am I using querysets incorrectly?
I used [] as an easy example to show that no matter what I put in nothing changes.
Please any insight would be invaluable
It should be noted that masterlistings has a primary key relationship that points to buildings. So a masterlisting belong to a building.
As pointed out by #zymud, queryset argument in PrimaryKeyRelatedField is used for validating field input for creating new entries.
Another solution for filtering out masterlistings_set is to use serializers.SerializerMethodField() as follows:
class BuildingsSerializer(serializers.ModelSerializer):
masterlisting_set = serializers.SerializerMethodField()
def get_masterlisting_set(self, obj):
return MasterListing.objects.filter(building=obj).values_list('pk',flat=True)
queryset in related field limits only acceptable values. So with queryset=[] you will not be able to add new values to masterlisting_set or create new Buildings.
UPDATE. How to use queryset for filtering
This is a little bi tricky - you need to rewrite ManyRelatedField and many_init method in your RelatedField.
# re-define ManyRelatedField `to_representation` method to filter values
# based on queryset
class FilteredManyRelatedField(serializers.ManyRelatedField):
def to_representation(self, iterable):
iterable = self.child_relation.queryset.filter(
pk__in=[value.pk for value in iterable])
return super(FilteredManyRelatedField, self).to_representation(iterable)
# use overridden FilteredManyRelatedField in `many_init`
class FilteredPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
#classmethod
def many_init(cls, *args, **kwargs):
kwargs['child_relation'] = cls(queryset=kwargs.pop('queryset'))
return FilteredManyRelatedField(*args, **kwargs)

How would you inherit from and override the django model classes to create a listOfStringsField?

I want to create a new type of field for django models that is basically a ListOfStrings. So in your model code you would have the following:
models.py:
from django.db import models
class ListOfStringsField(???):
???
class myDjangoModelClass():
myName = models.CharField(max_length=64)
myFriends = ListOfStringsField() #
other.py:
myclass = myDjangoModelClass()
myclass.myName = "bob"
myclass.myFriends = ["me", "myself", "and I"]
myclass.save()
id = myclass.id
loadedmyclass = myDjangoModelClass.objects.filter(id__exact=id)
myFriendsList = loadedclass.myFriends
# myFriendsList is a list and should equal ["me", "myself", "and I"]
How would you go about writing this field type, with the following stipulations?
We don't want to do create a field which just crams all the strings together and separates them with a token in one field like this. It is a good solution in some cases, but we want to keep the string data normalized so tools other than django can query the data.
The field should automatically create any secondary tables needed to store the string data.
The secondary table should ideally have only one copy of each unique string. This is optional, but would be nice to have.
Looking in the Django code it looks like I would want to do something similar to what ForeignKey is doing, but the documentation is sparse.
This leads to the following questions:
Can this be done?
Has it been done (and if so where)?
Is there any documentation on Django about how to extend and override their model classes, specifically their relationship classes? I have not seen a lot of documentation on that aspect of their code, but there is this.
This is comes from this question.
There's some very good documentation on creating custom fields here.
However, I think you're overthinking this. It sounds like you actually just want a standard foreign key, but with the additional ability to retrieve all the elements as a single list. So the easiest thing would be to just use a ForeignKey, and define a get_myfield_as_list method on the model:
class Friends(model.Model):
name = models.CharField(max_length=100)
my_items = models.ForeignKey(MyModel)
class MyModel(models.Model):
...
def get_my_friends_as_list(self):
return ', '.join(self.friends_set.values_list('name', flat=True))
Now calling get_my_friends_as_list() on an instance of MyModel will return you a list of strings, as required.
What you have described sounds to me really similar to the tags.
So, why not using django tagging?
It works like a charm, you can install it independently from your application and its API is quite easy to use.
I also think you're going about this the wrong way. Trying to make a Django field create an ancillary database table is almost certainly the wrong approach. It would be very difficult to do, and would likely confuse third party developers if you are trying to make your solution generally useful.
If you're trying to store a denormalized blob of data in a single column, I'd take an approach similar to the one you linked to, serializing the Python data structure and storing it in a TextField. If you want tools other than Django to be able to operate on the data then you can serialize to JSON (or some other format that has wide language support):
from django.db import models
from django.utils import simplejson
class JSONDataField(models.TextField):
__metaclass__ = models.SubfieldBase
def to_python(self, value):
if value is None:
return None
if not isinstance(value, basestring):
return value
return simplejson.loads(value)
def get_db_prep_save(self, value):
if value is None:
return None
return simplejson.dumps(value)
If you just want a django Manager-like descriptor that lets you operate on a list of strings associated with a model then you can manually create a join table and use a descriptor to manage the relationship. It's not exactly what you need, but this code should get you started.
Thanks for all those that answered. Even if I didn't use your answer directly the examples and links got me going in the right direction.
I am not sure if this is production ready, but it appears to be working in all my tests so far.
class ListValueDescriptor(object):
def __init__(self, lvd_parent, lvd_model_name, lvd_value_type, lvd_unique, **kwargs):
"""
This descriptor object acts like a django field, but it will accept
a list of values, instead a single value.
For example:
# define our model
class Person(models.Model):
name = models.CharField(max_length=120)
friends = ListValueDescriptor("Person", "Friend", "CharField", True, max_length=120)
# Later in the code we can do this
p = Person("John")
p.save() # we have to have an id
p.friends = ["Jerry", "Jimmy", "Jamail"]
...
p = Person.objects.get(name="John")
friends = p.friends
# and now friends is a list.
lvd_parent - The name of our parent class
lvd_model_name - The name of our new model
lvd_value_type - The value type of the value in our new model
This has to be the name of one of the valid django
model field types such as 'CharField', 'FloatField',
or a valid custom field name.
lvd_unique - Set this to true if you want the values in the list to
be unique in the table they are stored in. For
example if you are storing a list of strings and
the strings are always "foo", "bar", and "baz", your
data table would only have those three strings listed in
it in the database.
kwargs - These are passed to the value field.
"""
self.related_set_name = lvd_model_name.lower() + "_set"
self.model_name = lvd_model_name
self.parent = lvd_parent
self.unique = lvd_unique
# only set this to true if they have not already set it.
# this helps speed up the searchs when unique is true.
kwargs['db_index'] = kwargs.get('db_index', True)
filter = ["lvd_parent", "lvd_model_name", "lvd_value_type", "lvd_unique"]
evalStr = """class %s (models.Model):\n""" % (self.model_name)
evalStr += """ value = models.%s(""" % (lvd_value_type)
evalStr += self._params_from_kwargs(filter, **kwargs)
evalStr += ")\n"
if self.unique:
evalStr += """ parent = models.ManyToManyField('%s')\n""" % (self.parent)
else:
evalStr += """ parent = models.ForeignKey('%s')\n""" % (self.parent)
evalStr += "\n"
evalStr += """self.innerClass = %s\n""" % (self.model_name)
print evalStr
exec (evalStr) # build the inner class
def __get__(self, instance, owner):
value_set = instance.__getattribute__(self.related_set_name)
l = []
for x in value_set.all():
l.append(x.value)
return l
def __set__(self, instance, values):
value_set = instance.__getattribute__(self.related_set_name)
for x in values:
value_set.add(self._get_or_create_value(x))
def __delete__(self, instance):
pass # I should probably try and do something here.
def _get_or_create_value(self, x):
if self.unique:
# Try and find an existing value
try:
return self.innerClass.objects.get(value=x)
except django.core.exceptions.ObjectDoesNotExist:
pass
v = self.innerClass(value=x)
v.save() # we have to save to create the id.
return v
def _params_from_kwargs(self, filter, **kwargs):
"""Given a dictionary of arguments, build a string which
represents it as a parameter list, and filter out any
keywords in filter."""
params = ""
for key in kwargs:
if key not in filter:
value = kwargs[key]
params += "%s=%s, " % (key, value.__repr__())
return params[:-2] # chop off the last ', '
class Person(models.Model):
name = models.CharField(max_length=120)
friends = ListValueDescriptor("Person", "Friend", "CharField", True, max_length=120)
Ultimately I think this would still be better if it were pushed deeper into the django code and worked more like the ManyToManyField or the ForeignKey.
I think what you want is a custom model field.

Categories

Resources