I am trying to create a dynamic search in this ListView I have. My idea is to specify the fields and the type of search each time I try to inherit this view.
My problem is that every time I try to make a search, it works only on the first field of the tuple. In my example: requests__email is the first field, and when I print the object query_q after making a query for 'app', I get the following output:
(OR: (AND: ), ('requests__email__icontains', 'app'), (AND: ('requests__email__icontains', 'app'), ('status__icontains', 'app')), (AND: ('requests__email__icontains', 'app'), ('status__icontains', 'app'), ('license_type__name__icontains', 'app')))
I don't understand why, because I am using the operator I thought it would work |= in query_q |= Q(**query_kwargs). If I try to make a search based on the other attributes, status for example, the search doesn't work.
views.py
class DefaultListView(ListView):
searchable_fields = (
('requests__email', 'icontains'),
('status', 'icontains'),
('license_type__name', 'icontains'),
)
def get_queryset(self):
form = self.form_class(self.request.GET)
if form.is_valid():
if not self.searchable_fields:
raise AttributeError('searchable_fields has not been configured.')
term = form.cleaned_data['term']
query_kwargs = dict()
query_q = Q()
# Build a list of Q objects based on the searchable attribute
for field, search_type in self.searchable_fields:
query_kwargs["{0}__{1}".format(field, search_type)] = term
query_q |= Q(**query_kwargs)
ordering = self.get_ordering()
queryset = self.model.objects.filter(query_q)
if ordering:
return queryset.order_by(**ordering)
return queryset
return super(DefaultListView, self).get_queryset()
If you are wanting to build the query as X = Y OR W = Z it's because of
query_kwargs["{0}__{1}".format(field, search_type)] = term
You're adding more keys to query_kwargs with each iteration of the loop rather than re-creating the variable
Would suggest something like this
for field, search_type in self.searchable_fields:
query_q |= Q(**{"{0}__{1}".format(field, search_type): term})
Related
Context
I really want to, but I don't understand how I can limit an already existing Prefetch object
Models
class MyUser(AbstractUser):
pass
class Absence(Model):
employee = ForeignKey(MyUser, related_name='absences', on_delete=PROTECT)
start_date = DateField()
end_date = DateField()
View
class UserAbsencesListAPIView(ListAPIView):
queryset = MyUser.objects.order_by('first_name')
serializer_class = serializers.UserWithAbsencesSerializer
filterset_class = filters.UserAbsencesFilterSet
Filter
class UserAbsencesFilterSet(FilterSet):
first_name = CharFilter(lookup_expr='icontains', field_name='first_name')
from_ = DateFilter(method='filter_from', distinct=True)
to = DateFilter(method='filter_to', distinct=True)
What do I need
With the Request there are two arguments from_ and to. I should return Users with their Absences, which (Absences) are bounded by from_ and/or to intervals. It's very simple for a single argument, i can limit the set using Prefetch object:
def filter_from(self, queryset, name, value):
return queryset.prefetch_related(
Prefetch(
'absences',
Absence.objects.filter(Q(start_date__gte=value) | Q(start_date__lte=value, end_date__gte=value)),
)
)
Similarly for to.
But what if I want to get a limit by two arguments at once?
When the from_ attribute is requested - 'filter_from' method is executed; for the to argument, another method filter_to is executed.
I can't use prefetch_related twice, I get an exception ValueError: 'absences' lookup was already seen with a different queryset. You may need to adjust the ordering of your lookups..
I've tried using to_attr, but it looks like I can't access it in an un-evaluated queryset.
I know that I can find the first defined Prefetch in the _prefetch_related_lookups attribute of queryset, but is there any way to apply an additional filter to it or replace it with another Prefetch object so that I can end up with a query similar to:
queryset.prefetch_related(
Prefetch(
'absences',
Absence.objects.filter(
Q(Q(start_date__gte=from_) | Q(start_date__lte=from_, end_date__gte=from_))
& Q(Q(end_date__lte=to) | Q(start_date__lte=to, end_date__gte=to))
),
)
)
django-filter seems to have its own built-in filter for range queries:
More info here and here
So probably just easier to use that instead:
def filter_date_range(self, queryset, name, value):
if self.lookup_expr = "range":
#return queryset with specific prefetch
if self.lookup_expr = "lte":
#return queryset with specific prefetch
if self.lookup_expr = "gte":
#return queryset with specific prefetch
I haven't tested this and you may need to play around with the unpacking of value but it should get you most of the way there.
I have a choice filed and i need to sort it based on the order in CHOICES tuple and this model.py:
class MeetingMember(models.Model):
CHOICES = (
("H", "Host"),
("A", "Accepted"),
("R", "Rejected"),
("I", "Invited" )
)
status = models.CharField(max_length=9, choices=CHOICES, default="I")
i have already tried meta ordering :
class Meta:
ordering = ('status',)
but it is not working i need to sort it in Host,Accepted,Rejected,Invited
You can try to exploit the Replace function (https://docs.djangoproject.com/en/3.2/ref/models/database-functions/#replace). The strategy is to annotate a new field with a value built in a way that its alphabetical order matches the custom order you want in the original field:
from django.db.models import F, Value
from django.db.models.functions import Replace
# This generates a dictionary like {'0': 'H', '1': 'A', ...}
mapped_choices = {str(n): CHOICES[n][0] for n in range(len(CHOICES))}
# For each mapped choice we create a replacer
replacers = [
Replace('status_for_ordering', Value(original), Value(replacement))
for replacement, original in mapped_choices.items()
]
qs = MeetingMember.objects.all().annotate(status_for_ordering=F('status'))
for replacer in replacers:
qs = qs.annotate(status_for_ordering=replacer)
# Of course here you can still filter or do other operations before ordering
qs = qs.order_by('status_for_ordering')
This solution should work for your example but of course would need some adjustments in case the replacements starts to conflict each others (f.i. if one of your original status values contains a digit).
First time with Django. Trying to add an annotation to queryset:
class EnrollmentManager(models.Manager.from_queryset(EnrollmentCustomQuerySet)):
COURSE_DURATION = datetime.timedelta(days=183)
def get_queryset(self):
"""Overrides the models.Manager method"""
lookback = make_aware(datetime.datetime.today() - self.COURSE_DURATION)
qs = super(EnrollmentManager, self).get_queryset().annotate( \
is_expired=(Value(True)), output_field=models.BooleanField())
return qs
At the moment I am just trying to add an extra 'calculated' field on the returned queryset, which is hard-coded to True and the attribute/field should be called is_expired.
If I can get that to work, then Value(True) needs to be a derived value based on this expression:
F('enrolled') < lookback
But since 'enrolled' is a database field and lookback is calculated, how will I be able to do that?
Note
I tried this, which executes without throwing the error:
qs = super(EnrollmentManager, self).get_queryset().annotate( \
is_expired=(Value(True, output_field=models.BooleanField())))
and in the shell I can see it:
Enrollment.objects.all()[0].is_expired -> returns True
and I can add it to the serializer:
class EnrollmentSerializer(serializers.ModelSerializer):
is_active = serializers.SerializerMethodField()
is_current = serializers.SerializerMethodField()
is_expired = serializers.SerializerMethodField()
COURSE_DURATION = datetime.timedelta(days=183)
class Meta:
model = Enrollment
fields = ('id', 'is_active', 'is_current', 'is_expired')
def get_is_expired(self, obj):
return obj.is_expired
So it is possible...but how can I replace my hard-coded 'True" with a calculation?
UPDATE
Reading the documentation, it states:
"Annotates each object in the QuerySet with the provided list of query expressions. An expression may be a simple value, a reference to a field on the model (or any related models), or an aggregate expression (averages, sums, etc.) that has been computed over the objects that are related to the objects in the QuerySet."
A simple value - so, not a simple COMPUTED value then?
That makes me think this is not possible...
It seems like a pretty good use-case for a Case expression. I suggest getting as familiar as you can with these expression tools, they're very helpful!
I haven't tested this, but it should work. I'm assuming enrolled is a tz-aware datetime for when they first enrolled...
from django.db.models import Case, When, Value
def get_queryset(self):
"""Overrides the models.Manager method"""
lookback = make_aware(datetime.datetime.today() - self.COURSE_DURATION)
qs = super(EnrollmentManager, self).get_queryset().annotate(
is_expired=Case(
When(
enrolled__lt=lookback,
then=Value(True)
),
default=Value(False),
output_field=models.BooleanField()
)
)
You also don't have to pre-calculate the lookback variable. Check out ExpressionWrappers and this StackOverflow answer that addresses this.
ExpressionWrapper(
TruncDate(F('date1')) + datetime.timedelta(days=365),
output_field=DateField(),
)
I have this field:
operation = models.CharField(max_length=10, choices=OPERATIONS)
Having this filter works:
class OperationFilter(django_filters.Filter):
def filter(self, qs, value):
try:
qs = qs.filter(operation=value.upper())
except:
pass
return qs
With url:
/api/v1/operation/?operation=CREATE
But having the default filter (without an extra OperationFilter) fails with:
{
"operation": [
"Select a valid choice. %(value)s is not one of the available choices."
]
}
Why is a filter on a field with choices failing?
For other, non-choice fields, default filters are working fine:
/api/v1/operation/?recipient=recipient-19
EDIT
The OPERATIONS:
from enum import Enum
def enum_as_choices(enum_class):
"""From an enum class, generate choices for a django field"""
return ((entry, entry.value) for entry in enum_class)
class OperationType(Enum):
CREATE = 'CREATE'
STATUS = 'STATUS'
EXPAND = 'EXPAND'
DELETE = 'DELETE'
OPERATIONS = enum_as_choices(OperationType)
You are using django_filters package, I suggest reading docs, since you already have support for this
https://django-filter.readthedocs.io/en/master/ref/filters.html#choicefilter
Just point out your choices to the value suggested by the other answers (or check the example in docs)
The choices you written would be converted to this pythonic representation:
(
('OperationType.CREATE', 'CREATE'),
('OperationType.STATUS', 'STATUS'),
('OperationType.EXPAND', 'EXPAND'),
('OperationType.DELETE', 'DELETE')
)
As you can see the actual values stored in your operation field (in DB) are 'OperationType.CREATE', etc.
So you should change your choices to normal constant choices or you should filter by something like 'OperationType.CREATE' which is not a good option IMO.
also you can change your enum_as_choices method like this:
def enum_as_choices(enum_class):
"""From an enum class, generate choices for a django field"""
return ((entry.name, entry.value) for entry in enum_class)
You haven't defined a blank/default choice in your OPERATIONS. To do so, add something like this:
OPERATIONS = (
('', 'NONE'),
# the rest of your choices here...
)
But you would also need to update your model to be:
operation = models.CharField(max_length=10, choices=OPERATIONS, default='NONE')
I have the following case. I have a form with 3 fields which are submitted with POST method. Then the fields are captured and a search using Q is made on the database:
query = Model.objects.filter( Q(field1=field1) & Q(field2=field2) & Q(field3=field3))
The problem is that I would like to dynamically use the fields that are filled not the empty ones. That means that the query will contain one or two or three criteria depending on the user.
I have managed to perform the search I describe with nested if but considering adding extra fields it gets bigger and bigger.
thanks
You can write generic function to generate a query based on fields passed-in.
def generate_query(**kwargs):
query = Q()
for database_field, value in kwargs.items():
query_dict = {database_field:value}
if value:
query &= Q(**query_dict)
return query
And use it:
data = {"name":"john", "age__gte":25} # your post data
query = generate_query(**data)
objects = SomeModel.objects.filter(query)
# generally: SomeModel.objects.filter(request.POST)
Consider you have query field like below:
field = {'field1': 'value1', 'field2': 'value2', 'field3': None}
You can filter to remove null field as below:
non_empty_field = dict(filter(lambda x: x[1], field.items())) # output: {'field1': 'value1', 'field2': 'value2'}
For performing and query you can write
Model.objects.filter(**non_empty_field) # equivalent to Model.objects.filter(field1='value1', field2='value2')
to perform and query you don't need Q() object
For performing or query you can write:
Model.objects.filter(eval('|'.join(['(Q({}="{}"))'.format(i,j) for i,j in non_empty_field.items()])))
according to current field example above query will be equivalent to:
Model.objects.filter(Q(field1=value1) | Q(field2=value2))