Filter on a field with choices - django

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')

Related

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 )

How can I use a related field in a SlugRelatedField?

I have the following structures
class State(models.Model):
label = models.CharField(max_length=128)
....
class ReviewState(models.Model):
state = models.ForeignKey(State, on_delete=models.CASCADE)
...
class MySerializer(serializers.HyperlinkedModelSerializer):
state = serializers.SlugRelatedField(queryset=ReviewState.objects.all(), slug_field='state__label', required=False)
class Meta:
model = MyModel
fields = [
'id',
'state', # this points to a ReviewState object
....
]
What I'm trying to do is using the State object's label as the field instead. But it doesn't seem like djangorestframework likes the idea of using __ to lookup slug fields. Would it be possible to do this? If it was:
class MySerializer(serializers.HyperlinkedModelSerializer):
state = serializers.SlugRelatedField(queryset=State.objects.all(), slug_field='label', required=False)
that would be no problem, but I'm trying to use the ReviewState instead. I'm also trying to avoid having a ReviewStateSerializer as the resulting json would look like this
{...
'state': {'state': 'Pending'}}
}
Interesting question, and well put.
Using SlugRelatedField('state__label', queryset=...) works fine, with 1 caveat: its just calling queryset.get(state__label="x") which errors if there isn't exactly 1 match.
1) Write a custom field?
Inherit from SlugRelatedField and override to_internal_value(), maybe by calling .first() instead of .get(), or whatever other logic you need.
2) Re-evaluate this relationship, maybe its 1-to-1? a choice field?
I'm a bit confused on how this would all work, since you can have a "1 to many" with State => ReviewState. The default lookup (if you don't do #1) will throw an error when multiple matches occur.
Maybe this is a 1-to-1 situation with the model? Perhaps the ReviewState can use a ChoiceField instead of a table of states?
Perhaps the 'label' can be the PK of the State table, and also a SlugField rather than a non-unique CharField?
3) Write different serializers for the List and Create cases
DRF doesn't give us a built-in way to do this, but this reliance on "one serializer to do it all" is the cause of a lot of problems I see on SO. Its just really hard to get what you want without having different serializers for different cases. It's not hard to roll-your-own mixin to do it, but here's an example which uses an override:
from rest_framework import serializers as s
class MyCreateSerializer(s.ModelSerializer):
state = s.SlugRelatedField(...)
...
class MyListSerializer(s.ModelSerializer):
# use dotted notation, serializers read *object* attributes
state = s.CharField(source="state.state.label")
...
class MyViewSet(ModelViewSet):
queryset = MyModel.objects.select_related('state__state')
...
def get_serializer_class(self):
if self.action == "create":
return MyCreateSerializer
else:
return MyListSerializer

Django QuertySet.annotate() received non-expression - how to add a derived field based on model field?

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(),
)

Django Rest Framework Ordering on a SerializerMethodField

I have a Forum Topic model that I want to order on a computed SerializerMethodField, such as vote_count. Here are a very simplified Model, Serializer and ViewSet to show the issue:
# models.py
class Topic(models.Model):
"""
An individual discussion post in the forum
"""
title = models.CharField(max_length=60)
def vote_count(self):
"""
count the votes for the object
"""
return TopicVote.objects.filter(topic=self).count()
# serializers.py
class TopicSerializer(serializers.ModelSerializer):
vote_count = serializers.SerializerMethodField()
def get_vote_count(self, obj):
return obj.vote_count()
class Meta:
model = Topic
# views.py
class TopicViewSet(TopicMixin, viewsets.ModelViewSet):
queryset = Topic.objects.all()
serializer_class = TopicSerializer
Here is what works:
OrderingFilter is on by default and I can successfully order /topics?ordering=title
The vote_count function works perfectly
I'm trying to order by the MethodField on the TopicSerializer, vote_count like /topics?ordering=-vote_count but it seems that is not supported. Is there any way I can order by that field?
My simplified JSON response looks like this:
{
"id": 1,
"title": "first post",
"voteCount": 1
},
{
"id": 2,
"title": "second post",
"voteCount": 8
},
{
"id": 3,
"title": "third post",
"voteCount": 4
}
I'm using Ember to consume my API and the parser is turning it to camelCase. I've tried ordering=voteCount as well, but that doesn't work (and it shouldn't)
This is not possible using the default OrderingFilter, because the ordering is implemented on the database side. This is for efficiency reasons, as manually sorting the results can be incredibly slow and means breaking from a standard QuerySet. By keeping everything as a QuerySet, you benefit from the built-in filtering provided by Django REST framework (which generally expects a QuerySet) and the built-in pagination (which can be slow without one).
Now, you have two options in these cases: figure out how to retrieve your value on the database side, or try to minimize the performance hit you are going to have to take. Since the latter option is very implementation-specific, I'm going to skip it for now.
In this case, you can use the Count function provided by Django to do the count on the database side. This is provided as part of the aggregation API and works like the SQL COUNT function. You can do the equivalent Count call by modifying your queryset on the view to be
queryset = Topic.objects.annotate(vote_count=Count('topicvote_set'))
Replacing topicvote_set with your related_name for the field (you have one set, right?). This will allow you to order the results based on the number of votes, and even do filtering (if you want to) because it is available within the query itself.
This would require making a slight change to your serializer, so it pulls from the new vote_count property available on objects.
class TopicSerializer(serializers.ModelSerializer):
vote_count = serializers.IntegerField(read_only=True)
class Meta:
model = Topic
This will override your existing vote_count method, so you may want to rename the variable used when annotating (if you can't replace the old method).
Also, you can pass a method name as the source of a Django REST framework field and it will automatically call it. So technically your current serializer could just be
class TopicSerializer(serializers.ModelSerializer):
vote_count = serializers.IntegerField(read_only=True)
class Meta:
model = Topic
And it would work exactly like it currently does. Note that read_only is required in this case because a method is not the same as a property, so the value cannot be set.
Thanks #Kevin Brown for your great explanation and answer!
In my case I needed to sort a serializerMethodField called total_donation which is the sum of donations from the UserPayments table.
UserPayments has:
User as a foreignKey
sum which is an IntegerField
related_name='payments'
I needed to get the total donations per User but only donations that have a status of 'donated', not 'pending'. Also needed to filter out the payment_type coupon, which is related through two other foreign keys.
I was dumbfounded how to join and filter those donations and then be able to sort it via ordering_fields.
Thanks to your post I figured it out!
I realized it needed to be part of the original queryset in order to sort with ordering.
All I needed to do was annotate the queryset in my view, using Sum() with filters inside like so:
class DashboardUserListView(generics.ListAPIView):
donation_filter = Q(payments__status='donated') & ~Q(payments__payment_type__payment_type='coupon')
queryset = User.objects.annotate(total_donated=Sum('payments__sum', filter=donation_filter ))
serializer_class = DashboardUserListSerializer
pagination_class = DashboardUsersPagination
filter_backends = [filters.OrderingFilter]
ordering_fields = ['created', 'last_login', 'total_donated' ]
ordering = ['-created',]
I will put it here because the described case is not the only one.
The idea is to rewrite the list method of your Viewset to order by any of your SerializerMethodField(s) also without moving your logic from the Serializer to the ModelManager (especially when you work with several complex methods and/or related models)
def list(self, request, *args, **kwargs):
response = super().list(request, args, kwargs)
ordering = request.query_params.get('ordering')
if "-" in ordering:
response.data['results'] = sorted(response.data['results'], key=lambda k: (k[ordering.replace('-','')], ), reverse=True)
else:
response.data['results'] = sorted(response.data['results'], key=lambda k: (k[ordering], ))
return response

Best way to hardcode data in Django

I have a model, News, that has a ForeignKey to another model, Category.
The categories are defined with id, name, slug and a few more parameters.
My categories will never change in the future, so I am wondering why have I to store them in the database.
Is there any way to hardcode them inside the code, like using the choices or any other technique?
yes, choices: store pairs of ID+name as a tuple of tuples and in another structure any other parameters.
CATEGORIES = (
(1, "Some category"),
(2, "Another category"),
)
CATEGORIES_PARAMS = {
1: {'slug': 'param_value'},
2: {'slug': 'another_value'},
}
class News(models.Model):
category = models.IntegerField("category", choices=CATEGORIES)
...
then you can use other params following way:
CATEGORIES_PARAMS[news_instance.category]['slug']
or create simple wrapper class:
class Category(object):
def __init__(self, id):
self.id = id
def __getattr__(self, attr):
return CATEGORIES_PARAMS[self.id][attr]
cat = Category(news_instance.category)
print(cat.slug)
If you intend to create a relationship between your News and Category model using a ForeignKey, then there has to be a model Category. Now you're saying your Category will never change but in-case if they need to be changed in future, you need to redesign your code. To use django's full set of ORM features, it's always better to store it in the database and stick with the design.