Pass a custom queryset to serializer in Django Rest Framework - python

I am using Django rest framework 2.3
I have a class like this
class Quiz():
fields..
# A custom manager for result objects
class SavedOnceManager(models.Manager):
def filter(self, *args, **kwargs):
if not 'saved_once' in kwargs:
kwargs['saved_once'] = True
return super(SavedOnceManager, self).filter(*args, **kwargs)
class Result():
saved_once = models.NullBooleanField(default=False, db_index=True,
null=True)
quiz = models.ForeignKey(Quiz, related_name='result_set')
objects = SavedOnceManager()
As you see I have a custom manager on results so Result.objects.filter() will only return results that have save_once set to True
Now my Serializers look like this:
class ResultSerializer(serializers.ModelSerializer):
fields...
class QuizSerializer(serializers.ModelSerializer):
results = ResultSerializer(many=True, required=False, source='result_set')
Now if I serializer my quiz it would return only results that have saved_once set to True. But for a particular use case I want the serializer to return all objects. I have read that I can do that by passing a queryset parameter http://www.django-rest-framework.org/api-guide/relations/ in (further notes section). However when I try this
results = ResultSerializer(many=True, required=False, source='result_set',
queryset=
Result.objects.filter(
saved_once__in=[True, False]))
I get TypeError: __init__() got an unexpected keyword argument 'queryset'
And looking at the source code of DRF(in my version atleast) it does not accept a queryset argument.
Looking for some guidance on this to see if this is possible... thanks!

In my opinion, modifying filter like this is not a very good practice. It is very difficult to write a solution for you when I cannot use filter on the Result model without this extra filtering happening. I would suggest not modifying filter in this manner and instead creating a custom manager method which allows you to apply your filter in an obvious way where it is needed, eg/
class SavedOnceManager(models.Manager):
def saved_once(self):
return self.get_queryset().filter('saved_once'=True)
Therefore, you can query either the saved_once rows or the unfiltered rows as you would expect:
Results.objects.all()
Results.objects.saved_once().all()
Here is one way which you can use an additional queryset inside a serializer. However, it looks to me that this most likely will not work for you if the default manager is somehow filtering out the saved_once objects. Hence, your problem lies elsewhere.
class QuizSerializer(serializers.ModelSerializer):
results = serializers.SerializerMethodField()
def get_results(self, obj):
results = Result.objects.filter(id__in=obj.result_set)
return ResultSerializer(results, many=True).data

Related

what's the purpose of customizing django models

I read the following code for customizing Document model.
class DocumentQuerySet(models.QuerySet):
def pdfs(self):
return self.filter(file_type='pdf')
def smaller_than(self, size):
return self.filter(size__lt=size)
class DocumentManager(models.Manager):
def get_queryset(self):
return DocumentQuerySet(self.model, using=self._db) # Important!
def pdfs(self):
return self.get_queryset().pdfs()
def smaller_than(self, size):
return self.get_queryset().smaller_than(size)
class Document(models.Model):
name = models.CharField(max_length=30)
size = models.PositiveIntegerField(default=0)
file_type = models.CharField(max_length=10, blank=True)
objects = DocumentManager() #overriding the default model manager
Now suppose i want to retreive files of type pdf and size less than 1000.
Then i need to do the following:
Document.objects.pdfs().smaller_than(1000)
But what's the use of doing this even when i could have simply obtained the desired result by filtering the default model manager 'objects' using the following command:
Document.objects.filter(file_type='pdf', size__lt=1000)
What is the difference in the execution of above two commands?
Manager and Queryset methods are defined and used in order to use repeatable code.
If your use case only happens once, perhaps you are better off being explicit in your query, however if you repeat the exact same code elsewhere, perhaps you are better off making it a manager/queryset method.

How to force django model save method to lookup queryset in custom manager method?

I have a model named Run with a manager named RunManager and with a custom save() method as follows.
class RunManager(models.Manager):
use_for_related_fields = True
def get_queryset(self):
queryset = super(RunManager, self).get_queryset()
queryset = queryset.filter(archived=False)
return queryset
def unfiltered_runs(self):
queryset = super(RunManager, self).get_queryset()
return queryset
class Run(models.Model):
name = models.CharField(max_length=256)
archived = models.BooleanField(default=False)
objects = RunManager()
def save(self, *args, **kwargs):
# some business logic
super(Run, self).save(*args, **kwargs)
def archive(self):
# Some business logic
self.archived = True
self.save()
def recover_archived(self):
# Some business logic
self.archived = False
self.save()
This was an old code where the run.objects were used at several location, so to hide the archived runs I am using the RunManager.
Everything was working fine, but now we want to unarchive the runs. So I added the unfiltred_runs() method which shows the list of all the runs along with the archived runs. But when I run recove_archived() method i get following error
IntegrityError: UNIQUE constraint failed: run.id
I know the error is because the db is treating it as a new entry with same id.
I know I can completely override the save method but I want to avoid that.
So is there any way to make save method lookup in the unfiltered_runs() queryset instead of regular one.
By following #ivissani's suggestion I modified my recover_archived method as follows. And it is working flawlessly.
def recover_archived(self):
# Some business logic
Run.objects.unfiltered_runs().filter(pk=self.id).update(archived=False)

django-rest-framework non orm-based filtering

I am using DjangoRestApi and while it works like a charm with queryset (orm-based) views, I am struggling to make views that use different back-end to behave same way orm-based views are. Notably I want to add filters and have them cast and validated automatically.
Pseudo code below:
class NewsFilter(django_filters.FilterSet):
category = django_filters.NumberFilter(name='category')
limit = django_filters.NumberFilter(name='limit')
page = django_filters.NumberFilter(name='page')
class NewsView(generics.APIView):
filter_class = NewsFilter
def get(self, request):
filters = self.filter_class(??) # not sure, what to put here
payload = logic.get_business_news(**filters.data) # same
return Response(payload, status=status.HTTP_200_OK)
Any hint how to tackle problem will be appreciated.
Ultimate goal is to:
user types something into url or sends via POST, django-rest intercepts relevant values, extracts them, casts them into correct type and return as a dictionary
filters are displayed as they would if serializer was ORM based
The function signature to any single filter is like
class MyFilter(django_filters.Filter):
def filter(self,queryset,value):
[...]
The function signature to a FilterSet is:
def __init__(self, data=None, queryset=None, prefix=None, strict=None):
So, it looks like you pass in request.GET as data param and then pass in your queryset.

How can I use a Django F() expression in TastyPie's hydrate cycle?

I have a model with a version number in it. I want it to self-increment when new data is posted with an existing id via TastyPie. I'm currently doing this via the hydrate method, which works as long as two users don't try to update at once:
class MyResource(ModelResource):
...
def hydrate_version(self, bundle):
if 'id' in bundle.data:
target = self._meta.queryset.get(id=int(bundle.data['id']))
bundle.data['version'] = target.version+1
return bundle
I'd like to do this more robustly by using Django's F() expressions, e.g.:
def hydrate_version(self, bundle):
if 'id' in bundle.data:
from django.db.models import F
target = self._meta.queryset.get(id=int(bundle.data['id']))
bundle.data['version'] = F('version')+1
return bundle
However, this gives me an error:
TypeError: int() argument must be a string or a number, not 'ExpressionNode'
Is there a way to more robustly increment the version number with TastyPie?
thanks!
I would override the save() method for your Django Model instead, and perform the update there. That has the added advantage of ensuring the same behavior regardless of an update from tastypie or from the django/python shell.
def save(self, *args, **kwargs):
self.version = F('version') + 1
super(MyModel, self).save(*args, **kwargs)
This has been answered at github here, though I haven't tried this myself yet. To quote from that link:
You're setting bundle.data inside a hydrate method. Usually you modify bundle.obj in hydrate methods and bundle.data in dehydrate methods.
Also, those F objects are meant to be applied to Django model fields.
I think what you want is:
def hydrate_version(self, bundle):
if bundle.obj.id is not None:
from django.db.models import F
bundle.obj.version = F('version')+1
return bundle

Dynamically limiting queryset of related field

Using Django REST Framework, I want to limit which values can be used in a related field in a creation.
For example consider this example (based on the filtering example on https://web.archive.org/web/20140515203013/http://www.django-rest-framework.org/api-guide/filtering.html, but changed to ListCreateAPIView):
class PurchaseList(generics.ListCreateAPIView)
model = Purchase
serializer_class = PurchaseSerializer
def get_queryset(self):
user = self.request.user
return Purchase.objects.filter(purchaser=user)
In this example, how do I ensure that on creation the purchaser may only be equal to self.request.user, and that this is the only value populated in the dropdown in the form in the browsable API renderer?
I ended up doing something similar to what Khamaileon suggested here. Basically I modified my serializer to peek into the request, which kind of smells wrong, but it gets the job done... Here's how it looks (examplified with the purchase-example):
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
def get_fields(self, *args, **kwargs):
fields = super(PurchaseSerializer, self).get_fields(*args, **kwargs)
fields['purchaser'].queryset = permitted_objects(self.context['view'].request.user, fields['purchaser'].queryset)
return fields
class Meta:
model = Purchase
permitted_objects is a function which takes a user and a query, and returns a filtered query which only contains objects that the user has permission to link to. This seems to work both for validation and for the browsable API dropdown fields.
Here's how I do it:
class PurchaseList(viewsets.ModelViewSet):
...
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
context = self.get_serializer_context()
return serializer_class(*args, request_user=self.request.user, context=context, **kwargs)
class PurchaseSerializer(serializers.ModelSerializer):
...
def __init__(self, *args, request_user=None, **kwargs):
super(PurchaseSerializer, self).__init__(*args, **kwargs)
self.fields['user'].queryset = User._default_manager.filter(pk=request_user.pk)
The example link does not seem to be available anymore, but by reading other comments, I assume that you are trying to filter the user relationship to purchases.
If i am correct, then i can say that there is now an official way to do this. Tested with django rest framework 3.10.1.
class UserPKField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context['request'].user
queryset = User.objects.filter(...)
return queryset
class PurchaseSeriaizer(serializers.ModelSerializer):
users = UserPKField(many=True)
class Meta:
model = Purchase
fields = ('id', 'users')
This works as well with the browsable API.
Sources:
https://github.com/encode/django-rest-framework/issues/1985#issuecomment-328366412
https://medium.com/django-rest-framework/limit-related-data-choices-with-django-rest-framework-c54e96f5815e
I disliked the style of having to override the init method for every place where I need to have access to user data or the instance at runtime to limit the queryset. So I opted for this solution.
Here is the code inline.
from rest_framework import serializers
class LimitQuerySetSerializerFieldMixin:
"""
Serializer mixin with a special `get_queryset()` method that lets you pass
a callable for the queryset kwarg. This enables you to limit the queryset
based on data or context available on the serializer at runtime.
"""
def get_queryset(self):
"""
Return the queryset for a related field. If the queryset is a callable,
it will be called with one argument which is the field instance, and
should return a queryset or model manager.
"""
# noinspection PyUnresolvedReferences
queryset = self.queryset
if hasattr(queryset, '__call__'):
queryset = queryset(self)
if isinstance(queryset, (QuerySet, Manager)):
# Ensure queryset is re-evaluated whenever used.
# Note that actually a `Manager` class may also be used as the
# queryset argument. This occurs on ModelSerializer fields,
# as it allows us to generate a more expressive 'repr' output
# for the field.
# Eg: 'MyRelationship(queryset=ExampleModel.objects.all())'
queryset = queryset.all()
return queryset
class DynamicQuersetPrimaryKeyRelatedField(LimitQuerySetSerializerFieldMixin, serializers.PrimaryKeyRelatedField):
"""Evaluates callable queryset at runtime."""
pass
class MyModelSerializer(serializers.ModelSerializer):
"""
MyModel serializer with a primary key related field to 'MyRelatedModel'.
"""
def get_my_limited_queryset(self):
root = self.root
if root.instance is None:
return MyRelatedModel.objects.none()
return root.instance.related_set.all()
my_related_model = DynamicQuersetPrimaryKeyRelatedField(queryset=get_my_limited_queryset)
class Meta:
model = MyModel
The only drawback with this is that you would need to explicitly set the related serializer field instead of using the automatic field discovery provided by ModelSerializer. i would however expect something like this to be in rest_framework by default.
In django rest framework 3.0 the get_fields method was removed. But in a similar way you can do this in the init function of the serializer:
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Purchase
def __init__(self, *args, **kwargs):
super(PurchaseSerializer, self).__init__(*args, **kwargs)
if 'request' in self.context:
self.fields['purchaser'].queryset = permitted_objects(self.context['view'].request.user, fields['purchaser'].queryset)
I added the if check since if you use PurchaseSerializer as field in another serializer on get methods, the request will not be passed to the context.
First to make sure you only allow "self.request.user" when you have an incoming http POST/PUT (this assumes the property on your serializer and model is named "user" literally)
def validate_user(self, attrs, source):
posted_user = attrs.get(source, None)
if posted_user:
raise serializers.ValidationError("invalid post data")
else:
user = self.context['request']._request.user
if not user:
raise serializers.ValidationError("invalid post data")
attrs[source] = user
return attrs
By adding the above to your model serializer you ensure that ONLY the request.user is inserted into your database.
2) -about your filter above (filter purchaser=user) I would actually recommend using a custom global filter (to ensure this is filtered globally). I do something for a software as a service app of my own and it helps to ensure each http request is filtered down (including an http 404 when someone tries to lookup a "object" they don't have access to see in the first place)
I recently patched this in the master branch so both list and singular views will filter this
https://github.com/tomchristie/django-rest-framework/commit/1a8f07def8094a1e34a656d83fc7bdba0efff184
3) - about the api renderer - are you having your customers use this directly? if not I would say avoid it. If you need this it might be possible to add a custom serlializer that would help to limit the input on the front-end
Upon request # gabn88, as you may know by now, with DRF 3.0 and above, there is no easy solution.
Even IF you do manage to figure out a solution, it won't be pretty and will most likely fail on subsequent versions of DRF as it will override a bunch of DRF source which will have changed by then.
I forget the exact implementation I used, but the idea is to create 2 fields on the serializer, one your normal serializer field (lets say PrimaryKeyRelatedField etc...), and another field a serializer method field, which the results will be swapped under certain cases (such as based on the request, the request user, or whatever). This would be done on the serializers constructor (ie: init)
Your serializer method field will return a custom query that you want.
You will pop and/or swap these fields results, so that the results of your serializer method field will be assigned to the normal/default serializer field (PrimaryKeyRelatedField etc...) accordingly. That way you always deal with that one key (your default field) while the other key remains transparent within your application.
Along with this info, all you really need is to modify this: http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
I wrote a custom CustomQueryHyperlinkedRelatedField class to generalize this behavior:
class CustomQueryHyperlinkedRelatedField(serializers.HyperlinkedRelatedField):
def __init__(self, view_name=None, **kwargs):
self.custom_query = kwargs.pop('custom_query', None)
super(CustomQueryHyperlinkedRelatedField, self).__init__(view_name, **kwargs)
def get_queryset(self):
if self.custom_query and callable(self.custom_query):
qry = self.custom_query()(self)
else:
qry = super(CustomQueryHyperlinkedRelatedField, self).get_queryset()
return qry
#property
def choices(self):
qry = self.get_queryset()
return OrderedDict([
(
six.text_type(self.to_representation(item)),
six.text_type(item)
)
for item in qry
])
Usage:
class MySerializer(serializers.HyperlinkedModelSerializer):
....
somefield = CustomQueryHyperlinkedRelatedField(view_name='someview-detail',
queryset=SomeModel.objects.none(),
custom_query=lambda: MySerializer.some_custom_query)
#staticmethod
def some_custom_query(field):
return SomeModel.objects.filter(somefield=field.context['request'].user.email)
...
I did the following:
class MyModelSerializer(serializers.ModelSerializer):
myForeignKeyFieldName = MyForeignModel.objects.all()
def get_fields(self, *args, **kwargs):
fields = super(MyModelSerializer, self).get_fields()
qs = MyModel.objects.filter(room=self.instance.id)
fields['myForeignKeyFieldName'].queryset = qs
return fields
I looked for a solution where I can set the queryset upon creation of the field and don't have to add a separate field class. This is what I came up with:
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Purchase
fields = ["purchaser"]
def get_purchaser_queryset(self):
user = self.context["request"].user
return Purchase.objects.filter(purchaser=user)
def get_extra_kwargs(self):
kwargs = super().get_extra_kwargs()
kwargs["purchaser"] = {"queryset": self.get_purchaser_queryset()}
return kwargs
The main issue for tracking suggestions regarding this seems to be drf#1985.
Here's a re-usable generic serializer field that can be used instead of defining a custom field for every use case.
class DynamicPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
"""A PrimaryKeyRelatedField with ability to set queryset at runtime.
Pass a function in the `queryset_fn` kwarg. It will be passed the serializer `context`.
The function should return a queryset.
"""
def __init__(self, queryset_fn=None, **kwargs):
assert queryset_fn is not None, "The `queryset_fn` argument is required."
self.queryset_fn = queryset_fn
super().__init__(**kwargs)
def get_queryset(self):
return self.queryset_fn(context=self.context)
Usage:
class MySerializer(serializers.ModelSerializer):
my_models = DynamicPrimaryKeyRelatedField(
queryset_fn=lambda context: MyModel.objects.visible_to_user(context["request"].user)
)
# ...
Same works for serializers.SlugRelatedField.

Categories