How to cache SerializerMethodField result - python

I have the below serializer:
class OrderItemResponseSerializer(serializers.ModelSerializer):
prepack_qty = serializers.SerializerMethodField()
product_description = serializers.SerializerMethodField()
class Meta:
model = OrderItem
fields = (
"product",
"qty_ordered",
"qty_approved",
"status",
"prepack_qty",
"product_description"
)
def get_prepack_qty(self, obj):
return obj.product.prepack_quantity
def get_product_description(self, obj):
return obj.product.product_description
When I make a get request to /orders, I make a lot of SQL queries to the database because different orders may contain the same product. How can I cache the result of get_prepack_qty and get_product_description methods? I tried to use #cached_property this way:
class OrderItem(models.Model):
...
#cached_property
def item_product_description(self):
return self.product.product_description
But the number of requests to the database remained the same.

Well, first of all, I should say that what you have implemented in this piece of code below:
...
#cached_property
def item_product_description(self):
return self.product.product_description
And using #cached_property alone doesn't cache the data for you, you just created a property in the model for get_product_description serializer method, And this does not reduce the volume and number of your queries to the database at all; Of course you need .bind() method in your serializer method like below:
class BlobSerializer(SerializerCacheMixin, serializers.Serializer):
blobs = serializers.SerializerMethodField()
def get_blobs(self, instance):
# recursive serializer
serializer = self.__class__(instance.results, many=True)
serializer.bind('*', self)
return serializer.data
But in order to cache the result of this method As you asked in your question there is a good project in Pypi called drf-serializer-cache you can use it easily for this purpose, for example, the following piece of code is taken from the document of this project:
from drf_serializer_cache import SerializerCacheMixin
from rest_framework import serializer
class ResultSerializer(SerializerCacheMixin, serializers.Serializer):
results = serializers.SerializerMethodField()
def get_results(self, instance):
# recursive serializer
serializer = self.__class__(instance.results, many=True)
serializer.bind('*', self) # bind call is essential for >efficient cache!
return serializer.data
Also if you want to implement it yourself in your project Seeing the implementation of SerializerCacheMixin object in this project can help you a lot or even use it directly.

You can leverage Django's cache framework to cache the result of a SerializerMethodField. It would look something like this:
from django.core.cache import cache
class MySerializer(serializers.Serializer):
my_field = serializers.SerializerMethodField()
def get_my_field(self, obj):
# Get cached value
value = cache.get("my_field_%s" % obj.pk)
if value is None:
# Value is not cached, compute it
value = ...
# Cache the value
cache.set("my_field_%s" % obj.pk, value, 3600)
return value
This will cache the result of the field for 1 hour (3600 seconds), using a cache key that depends on the object's primary key.
You can of course adapt the caching logic to your exact use case.

Related

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)

How to filter ModelAdmin autocomplete_fields results with the context of limit_choices_to

I have a situation where I wish to utilize Django's autocomplete admin widget, that respects a referencing models field limitation.
For example I have the following Collection model that has the attribute kind with specified choices.
class Collection(models.Model):
...
COLLECTION_KINDS = (
('personal', 'Personal'),
('collaborative', 'Collaborative'),
)
name = models.CharField()
kind = models.CharField(choices=COLLECTION_KINDS)
...
Another model ScheduledCollection references Collection with a ForeignKey field that implements limit_choices_to option. The purpose of this model is to associate meta data to a Collection for a specific use case.
class ScheduledCollection(models.Model):
...
collection = models.ForeignKey(Collection, limit_choices_to={'kind': 'collaborative'})
start_date = models.DateField()
end_date = models.DateField()
...
Both models are registered with a ModelAdmin. The Collection model implements search_fields.
#register(models.Collection)
class CollectionAdmin(ModelAdmin):
...
search_fields = ['name']
...
The ScheduledCollection model implements autocomplete_fields
#register(models.ScheduledCollection)
class ScheduledCollectionAdmin(ModelAdmin):
...
autocomplete_fields = ['collection']
...
This works but not entirely as expected. The autocomplete retrieves results from a view generated by the Collection model. The limit_choices_to do not filter the results and are only enforced upon save.
It has been suggested to implement get_search_results or get_queryset on the CollectionAdmin model. I was able to do this and filter the results. However, this changes Collection search results across the board. I am unaware of how to attain more context within get_search_results or get_queryset to conditionally filter the results based upon a relationship.
In my case I would like to have several choices for Collection and several meta models with different limit_choices_to options and have the autocomplete feature respect these restrictions.
I don't expect this to work automagically and maybe this should be a feature request. At this point I am at a loss how to filter the results of a autocomplete with the respect to a choice limitation (or any condition).
Without using autocomplete_fields the Django admin's default <select> widget filters the results.
Triggering off the http referer was ugly so I made a better version: subclass the AutocompleteSelect and send extra query parameters to allow get_search_results to lookup the correct limit_choices_to automagically. Simply include this mixin in your ModelAdmin (for both source and target models). As a bonus it also adds a delay to the ajax requests so you don't spam the server as you type in the filter, makes the select wider and sets the search_fields attribute (to 'translations__name' which is correct for my system, customise for yours or omit and set individually on the ModelAdmins as before):
from django.contrib.admin import widgets
from django.utils.http import urlencode
from django.contrib.admin.options import ModelAdmin
class AutocompleteSelect(widgets.AutocompleteSelect):
"""
Improved version of django's autocomplete select that sends an extra query parameter with the model and field name
it is editing, allowing the search function to apply the appropriate filter.
Also wider by default, and adds a debounce to the ajax requests
"""
def __init__(self, rel, admin_site, attrs=None, choices=(), using=None, for_field=None):
super().__init__(rel, admin_site, attrs=attrs, choices=choices, using=using)
self.for_field = for_field
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
attrs.update({
'data-ajax--delay': 250,
'style': 'width: 50em;'
})
return attrs
def get_url(self):
url = super().get_url()
url += '?' + urlencode({
'app_label': self.for_field.model._meta.app_label,
'model_name': self.for_field.model._meta.model_name,
'field_name': self.for_field.name
})
return url
class UseAutocompleteSelectMixin():
"""
To avoid ForeignKey fields to Event (such as on ReportColumn) in admin from pre-loading all events
and thus being really slow, we turn them into autocomplete fields which load the events based on search text
via an ajax call that goes through this method.
Problem is this ignores the limit_choices_to of the original field as this ajax is a general 'search events'
without knowing the context of what field it is populating. Someone else has exact same problem:
https://stackoverflow.com/questions/55344987/how-to-filter-modeladmin-autocomplete-fields-results-with-the-context-of-limit-c
So fix this by adding extra query parameters on the autocomplete request,
and use these on the target ModelAdmin to lookup the correct limit_choices_to and filter with it.
"""
# Overrides django.contrib.admin.options.ModelAdmin#formfield_for_foreignkey
# Is identical except in case db_field.name is in autocomplete fields it constructs our improved AutocompleteSelect
# instead of django's and passes it extra for_field parameter
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name in self.get_autocomplete_fields(request):
db = kwargs.get('using')
kwargs['widget'] = AutocompleteSelect(db_field.remote_field, self.admin_site, using=db, for_field=db_field)
if 'queryset' not in kwargs:
queryset = self.get_field_queryset(db, db_field, request)
if queryset is not None:
kwargs['queryset'] = queryset
return db_field.formfield(**kwargs)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
# In principle we could add this override in a different mixin as adding the formfield override above is needed on
# the source ModelAdmin, and this is needed on the target ModelAdmin, but there's do damage adding everywhere so combine them.
def get_search_results(self, request, queryset, search_term):
if 'app_label' in request.GET and 'model_name' in request.GET and 'field_name' in request.GET:
from django.apps import apps
model_class = apps.get_model(request.GET['app_label'], request.GET['model_name'])
limit_choices_to = model_class._meta.get_field(request.GET['field_name']).get_limit_choices_to()
if limit_choices_to:
queryset = queryset.filter(**limit_choices_to)
return super().get_search_results(request, queryset, search_term)
search_fields = ['translations__name']
I had the exact same problem. It's a bit hacky, but here's my solution:
Override get_search_results of the ModelAdmin you are searching for and want to filter
Use the request referer header to get the magical context you need to apply the appropriate filter based on the source of the relationship
Grab the limit_choices_to from the appropriate ForeignKey's _meta
Pre-filter the queryset and then pass to super method.
So for your models:
#register(models.Collection)
class CollectionAdmin(ModelAdmin):
...
search_fields = ['name']
def get_search_results(self, request, queryset, search_term):
if '<app_name>/scheduledcollection/' in request.META.get('HTTP_REFERER', ''):
limit_choices_to = ScheduledCollection._meta.get_field('collection').get_limit_choices_to()
queryset = queryset.filter(**limit_choices_to)
return super().get_search_results(request, queryset, search_term)
A shortcoming of this approach is the only context we have is the model being edited in admin, rather than which field of the model, so if your ScheduledCollection model has 2 collection autocomplete fields (say personal_collection and collaborative_collection) with different limit_choices_to we can't infer this from the referer header and treat them differently. Also inline admins will have the referer url based on the parent thing they are an inline for, rather than reflecting their own model. But it works in the basic cases.
Hopefully a new version of Django will have a cleaner solution, such as the autocomplete select widget sending an extra query parameter with the model and field name it is editing so that get_search_results can accurately look up the required filters instead of (potentially inaccurately) inferring from the referer header.
Here is another solution to get only a subset of choices in the auto-complete field. This solution does not change the default behavior for the main model (Collection), so you can still have other views using autocomplete with the full set in your app.
Here is how it works:
Proxy model for Collection with manager
Create a proxy model to represent a subset of Collection, e.g. CollaborativeCollection to represent collections that are of kind "collaborative". You will also need a manager to restrict the initial queryset of your proxy model to the intended subset.
class CollaborativeCollectionManager(models.Manager):
def get_queryset(self):
return (
super()
.get_queryset()
.filter(kind="collaborative")
)
class CollaborativeCollection(models.Model):
class Meta:
proxy = True
objects = CollaborativeCollectionManager()
Updating foreign key to use proxy model
Next update the foreign key in ScheduledCollection to use the proxy model instead. Note that you can remove the limit_choices_to feature if you don't need it for anything else.
class ScheduledCollection(models.Model):
...
collection = models.ForeignKey(CollaborativeCollection)
start_date = models.DateField()
end_date = models.DateField()
...
Define admin model for Proxy
Finally define the admin model for the proxy.
#admin.register(CollaborativeCollection)
class CollaborativeCollectionAdmin(admin.ModelAdmin):
search_fields = ["name"]
Note that instead of the manager, you could also define a custom get_search_results() in the admin model. However, I found that the manager approach appears to be more performant. And it also is conceptually more sounds, since with that all queries to CollaborativeCollection will only return collaborative collections.
My solution is to wrap get_url method on the widget.
Create a util method as shown below.
def wrap_get_url(original_get_url, extra_url_params: QueryDict) -> Callable:
def get_url_with_extra_url_params(*args, **kwargs) -> str:
url: str = original_get_url(*args, **kwargs)
scheme, netloc, url, params, query, fragment = tuple(urlparse(url))
query = QueryDict(query_string=query, mutable=True)
query.update(extra_url_params)
url_parts = (scheme, netloc, url, params, query.urlencode(), fragment)
return urlunparse(url_parts)
return get_url_with_extra_url_params
Create a custom form for your model admin.
class ExampleModelAdminForm(forms.ModelForm):
class Meta:
model = ExampleModel
exclude: List[str] = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = getattr(self, "instance", None)
# Check for RelatedWidgetWrapper
if widget := getattr(self.fields["target_model"].widget, "widget", None):
query = QueryDict(mutable=True)
query["example_model_id"] = instance.pk
widget.get_url = wrap_get_url(
original_get_url=widget.get_url,
extra_url_params=query,
)
class ExampleModelAdmin(admin.ModelAdmin):
form = forms.ExampleModelAdminForm
autocomplete_fields = ("target_model",)
On the target model admin.
class TargetModelAdmin(admin.ModelAdmin):
search_fields = ("name", ) # Define your search fields
def get_search_results(self, request, queryset, search_term) -> tuple[QuerySet, bool]:
qs: QuerySet
duplicate: bool
qs, duplicate = super(TargetModelAdmin, self).get_search_results(request, queryset, search_term)
# Get Example model id from previous admin page in order to filter the queryset
if example_model_id := request.GET.get("example_account_id", None):
example_model: ExampleModel = ExampleModel.objects.get(
id=example_model_id
)
qs = qs.filter(field=example_model.field) # Filter your qs here
return qs, duplicate
With Django 3.2, the solution proposed by #Uberdude does not work anymore because AutocompleteSelect's constructor now takes a field rather than a relation.
Here is the updated code needed for the formfield_for_foreignkey method:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name in self.get_autocomplete_fields(request) or\
db_field.name in self.get_autocomplete_cb_fields(request):
db = kwargs.get('using')
if db_field.name in self.get_autocomplete_cb_fields(request):
kwargs['widget'] = AutocompleteSelectCb(
db_field, self.admin_site, using=db, for_field=db_field)
else:
kwargs['widget'] = AutocompleteSelect(
db_field, self.admin_site, using=db, for_field=db_field)
if 'queryset' not in kwargs:
queryset = self.get_field_queryset(db, db_field, request)
if queryset is not None:
kwargs['queryset'] = queryset
return db_field.formfield(**kwargs)
return super().formfield_for_foreignkey(db_field, request, **kwargs)

Pass a custom queryset to serializer in Django Rest Framework

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

Django Rest Framework - Read nested data, write integer

So far I'm extremely happy with Django Rest Framework, which is why I alsmost can't believe there's such a large omission in the codebase. Hopefully someone knows of a way how to support this:
class PinSerializer(serializers.ModelSerializer):
item = ItemSerializer(read_only=True, source='item')
item = serializers.IntegerSerializer(write_only=True)
class Meta:
model = Pin
with the goal
The goal here is to read:
{pin: item: {name: 'a', url: 'b'}}
but to write using an id
{pin: item: 10}
An alternative would be to use two serializers, but that looks like a really ugly solution:
django rest framework model serializers - read nested, write flat
Django lets you access the Item on your Pin with the item attribute, but actually stores the relationship as item_id. You can use this strategy in your serializer to get around the fact that a Python object cannot have two attributes with the same name (a problem you would encounter in your code).
The best way to do this is to use a PrimaryKeyRelatedField with a source argument. This will ensure proper validation gets done, converting "item_id": <id> to "item": <instance> during field validation (immediately before the serializer's validate call). This allows you to manipulate the full object during validate, create, and update methods. Your final code would be:
class PinSerializer(serializers.ModelSerializer):
item = ItemSerializer(read_only=True)
item_id = serializers.PrimaryKeyRelatedField(write_only=True,
source='item',
queryset=Item.objects.all())
class Meta:
model = Pin
fields = ('id', 'item', 'item_id',)
Note 1: I also removed source='item' on the read-field as that was redundant.
Note 2: I actually find it rather unintuitive that Django Rest is set up such that a Pin serializer without an Item serializer specified returns the item_id as "item": <id> and not "item_id": <id>, but that is beside the point.
This method can even be used with forward and reverse "Many" relationships. For example, you can use an array of pin_ids to set all the Pins on an Item with the following code:
class ItemSerializer(serializers.ModelSerializer):
pins = PinSerializer(many=True, read_only=True)
pin_ids = serializers.PrimaryKeyRelatedField(many=True,
write_only=True,
source='pins',
queryset=Pin.objects.all())
class Meta:
model = Item
fields = ('id', 'pins', 'pin_ids',)
Another strategy that I previously recommended is to use an IntegerField to directly set the item_id. Assuming you are using a OneToOneField or ForeignKey to relate your Pin to your Item, you can set item_id to an integer without using the item field at all. This weakens the validation and can result in DB-level errors from constraints being violated. If you want to skip the validation DB call, have a specific need for the ID instead of the object in your validate/create/update code, or need simultaneously writable fields with the same source, this may be better, but I wouldn't recommend anymore. The full line would be:
item_id = serializers.IntegerField(write_only=True)
If you are using DRF 3.0 you can implement the new to_internal_value method to override the item field to change it to a PrimaryKeyRelatedField to allow the flat writes. The to_internal_value takes unvalidated incoming data as input and should return the validated data that will be made available as serializer.validated_data. See the docs: http://www.django-rest-framework.org/api-guide/serializers/#to_internal_valueself-data
So in your case it would be:
class ItemSerializer(ModelSerializer):
class Meta:
model = Item
class PinSerializer(ModelSerializer):
item = ItemSerializer()
# override the nested item field to PrimareKeyRelatedField on writes
def to_internal_value(self, data):
self.fields['item'] = serializers.PrimaryKeyRelatedField(queryset=Item.objects.all())
return super(PinSerializer, self).to_internal_value(data)
class Meta:
model = Pin
Two things to note: The browsable web api will still think that writes will be nested. I'm not sure how to fix that but I only using the web interface for debug so not a big deal. Also, after you write the item returned will have flat item instead of the nested one. To fix that you can add this code to force the reads to use the Item serializer always.
def to_representation(self, obj):
self.fields['item'] = ItemSerializer()
return super(PinSerializer, self).to_representation(obj)
I got the idea from this from Anton Dmitrievsky's answer here: DRF: Simple foreign key assignment with nested serializers?
You can create a Customized Serializer Field (http://www.django-rest-framework.org/api-guide/fields)
The example took from the link:
class ColourField(serializers.WritableField):
"""
Color objects are serialized into "rgb(#, #, #)" notation.
"""
def to_native(self, obj):
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
def from_native(self, data):
data = data.strip('rgb(').rstrip(')')
red, green, blue = [int(col) for col in data.split(',')]
return Color(red, green, blue)
Then use this field in your serializer class.
I create a Field type that tries to solve the problem of the Data Save requests with its ForeignKey in Integer, and the requests to read data with nested data
This is the class:
class NestedRelatedField(serializers.PrimaryKeyRelatedField):
"""
Model identical to PrimaryKeyRelatedField but its
representation will be nested and its input will
be a primary key.
"""
def __init__(self, **kwargs):
self.pk_field = kwargs.pop('pk_field', None)
self.model = kwargs.pop('model', None)
self.serializer_class = kwargs.pop('serializer_class', None)
super().__init__(**kwargs)
def to_representation(self, data):
pk = super(NestedRelatedField, self).to_representation(data)
try:
return self.serializer_class(self.model.objects.get(pk=pk)).data
except self.model.DoesNotExist:
return None
def to_internal_value(self, data):
return serializers.PrimaryKeyRelatedField.to_internal_value(self, data)
And so it would be used:
class PostModelSerializer(serializers.ModelSerializer):
message = NestedRelatedField(
queryset=MessagePrefix.objects.all(),
model=MessagePrefix,
serializer_class=MessagePrefixModelSerializer
)
I hope this helps you.

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