I want to write custom get_queryset() method for serializer based on query params.
Here is my serializer:
class SearchRequestSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255, required=False)
nickname = serializers.RegexField(
r'^(?!(.*?\_){2})(?!(.*?\.){2})[A-Za-z0-9\._]{3,24}$',
max_length=24,
min_length=3,
required=False,
)
modelA_id = serializers.CharField(max_length=11, min_length=11,
required=False)
def validate_modelA_id(self, value):
queryset = modelA.objects.filter(id=value)
if queryset.exists():
return queryset.first()
else:
raise serializers.ValidationError(_('Not found'))
If object of modelA exists - validation will return an instance. But I
don't want to perform the same query in get_queryset() in my if branch.
def get_queryset(self):
name = self.request.query_params.get('name', None)
nickname = self.request.query_params.get('nickname', None)
queryset = User.objects.filter(Q(name__contains=name)|Q(nickname__contains=nickname))
if 'modelA_id' in self.request.query_params:
# in this case will be annotated extra field to queryset
# extra field will be based on 'modelA' instance which should be returned by serializer
return queryset
I found only one solution - add the following line in my GET method:
self.serializer = self.get_serializer()
Than it will be possible to get validated values in my get_queryset() method. But PyCharm don't like this solution
I'm under strong impression that you are misusing the Serializer. After quick analysis of your issue i think you need DRF filtering
Serializers process request.data which under the hood is just Django request.POST and request.FILES yet in your get_queryset implementation you make lookups in request.query_params which in Django terms is request.GET. Check the DRF docs on this.
In order to achieve what you need with Serializers you would have to abuse the Views, Serializer Fields and Serializer itself. It is simply not what its supposed to do.
Additionaly I don't think that you need so much validation on search.
Customization and use of Django Filter should solve your problem.
class UserFilter(filters.FilterSet):
class Meta:
model = User
fields = ['name', 'nickname', 'modelA_id']
def filter_queryset(self, queryset):
name = self.form.cleaned_data.get('name', None)
nickname = self.form.cleaned_data.get('nickname', None)
queryset = queryset.filter(Q(name__contains=name)|Q(nickname__contains=nickname))
if 'modelA_id' in self.form.cleaned_data:
# in this case will be annotated extra field to queryset
# extra field will be based on 'modelA' instance which should be returned by serializer
return queryset
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
filterset_class = UserFilter
NOTE I did not test code above but it should show you how to tackle this problem.
How is about user = get_object_or_404(queryset, id=ModelA_id). It looks better as for me.
get_object_or_404 will catch an object you need, or will raise Not Found responce.
Related
Today i have a really strange behoviour. I have added a custom mixin to my User Viewset to filter the users by username and password and other fields. the code look like the following:
class MultipleFieldLookupMixin(object):
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
if field in self.kwargs.keys(): # Ignore empty fields.
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter) # Lookup the object
return obj
class UserView(MultipleFieldLookupMixin, viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
lookup_fields = ['username', 'pwd', 'token_confirm', 'email', 'token_cng_pwd']
i would like the list operation to filter the queryset in every given parameter (so None if none is avalable and all if all are available).
But the list endpoint continues to show every user to me, without filtering them.
Where am i wrong? Do i have to manually set a filterbackend? am I doing some stupid mistake?
Thanks a lot
Maybe do i have to put those parameters inside the urls.py?
I have changed the urls.py not but it throws me an exception:
router.register('user', views.UserView)
router.register('user/username/{username}/', views.UserView, username = 'username')
router.register('user/username/{username}/pwd/{pwd}/', views.UserView, username = 'username', pwd = 'pwd')
router.register('user/email/{email}/', views.Userview, email = 'email')
router.register('user/token_confirm/{token_confirm}/', views.UserView, token_confirm = 'token_confirm')
router.register('user/token_cng_pwd/{token_cng_pwd}/', views.UserView, token_cng_pwd = 'token_cng_pwd')
Exception:
router.register('user/username/{username}', views.UserView, username = 'username')
TypeError: register() got an unexpected keyword argument 'username'
I didn't mean to filter the query but i'll give a try!
The get_object return a single object instance.
the docstring say you can look for single object with one or more fields (attributes).
obj = get_object_or_404(queryset, **filter)
to filter list use django_filters it build exactly for this purpose, it is very powerful and easy to use.
from rest_framework import routers, serializers, viewsets
from rest_framework import filters
import django_filters.rest_framework
from .serializer import UserSerializer, User
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
filter_backends = [django_filters.rest_framework.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['username' , 'first_name']
search_fields = ['username']
ordering_fields = ['username']
ordering = ['username']
If your still curious or want to build a custom filtering for list view, then override the get_queryset() method of the viewset.
I have a serializer that has a nested serializer field. I had set up eager loading and everything was working great.
However, I had do add some custom filtering to the nested field, which required a SerializerMethodField.
After that change, the prefetch_related eager loading is no longer working. How can I optimize a serializer with SerializerMethodField?
Here was my initial, working setup:
# views.py
class MyView(generics.ListAPIView):
serializer_class = WorkingSerializer
def get_queryset(self):
queryset = MyModel.objects.all()
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
# serializers.py
class WorkingSerializer(serializers.ModelSerializer):
related_field_name = CustomSerializer(many=True)
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related('related_field_name')
return queryset
And here is my changed serializer that doesn't work:
# serializers.py
class NotWorkingSerializer(serializers.ModelSerializer):
related_field_name = serializers.SerializerMethodField('get_related_field')
def get_related_field(self, instance):
queryset = instance.related_field_name.all()
# some filtering done here
return queryset
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related('related_field_name')
return queryset
Instead of doing the prefetch request in the serializer, you can do that in your actual queryset coming from the View. You can override get_queryset method with the custom query. Not sure what is your actual query but for an example you can do something like:
def get_queryset(self):
queryset = Model.objects.preftech_related("related_field")
return queryset
and this queryset will already have your related field in it. You won't have to write custom logic in the serializer.
I'm wondering which is the best way to solve this, I have a nested serializer like this:
serializers.py:
class PaymentMethodSerializer(serializers.ModelSerializer):
data = serializers.JSONField()
payment_type = PaymentTypeSerializer(required=False)
Then, the view looks something like this:
class PaymentMethodView(APIView):
def put(self, request, id):
try:
payment_method = PaymentMethod.objects.get(id=id)
except ObjectDoesNotExist:
return Response("No PaymentMethod with that id", status=status.HTTP_404_NOT_FOUND)
payment_method_serialized = PaymentMethodSerializer(instance=payment_method, data=request.data)
payment_type = request.data.get("payment_type", None)
if payment_type:
try:
payment_method_type = PaymentType.objects.get(id=payment_type)
except ObjectDoesNotExist:
payment_method_type = None
else:
payment_method_type = None
# Now, call is_valid()
if payment_method_serialized.is_valid():
payment_method_serialized.save(payment_type=payment_method_type)
return Response(payment_method_serialized.data, status=status.HTTP_200_OK)
is_valid() returns False and this error:
{"payment_type":{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}}
I understand it, I'm giving the serializer a pk. However I don't want to create a new serializer with a PrimaryKeyRelatedField instead of the nested relationship just for this. How can I get the PaymentType that corresponds to that pk and then add that object to the request.data dictionary so that is_valid doesn't fail? is it the best way to solve this?
Suppose payment_type is ForeignKey field in a PaymentMethod model, with null=True because of required=False.
If you provide only a pk so you don't need serializer for it field, you can just write in fields, like all other fields.
class PaymentMethodSerializer(serializers.ModelSerializer):
data = serializers.JSONField()
class Meta:
model = PaymentMethod
fields = ('data', 'payment_type')
It will accept pk or None. If you want to provide special representation for your PaymentType model, you can override a to_representation() method.
class PaymentMethodSerializer(serializers.ModelSerializer):
...
def to_representation(self, instance):
representation = super(PaymentMethodSerializer, self).to_representation(instance)
representation['payment_type'] = PaymentTypeSerializer(instance.payment_type).data
return representation
Now it will use PaymentTypeSerializer for representation of related PaymentType model.
You can move you payment_type validation in a serializer though. Override to_internal_value, check provided PaymentType pk and throw an exception if it wrong and then catch that exception in a view.
I'm working myself through a lot of examples, but I can't find a way that works 100%.
class QuestionViewSet(viewsets.ModelViewSet):
queryset = QNAQuestion.objects.all()
serializer_class = QuestionSerializer
permission_classes = (IsOwnerOrReadOnly, )
filter_fields = ('id', 'user')
filter_backends = (filters.DjangoFilterBackend, filters.OrderingFilter)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
This so far works fine, but it still requires the user_id to be given by the user input even though it's ignored and relpaced by request.user.
class QuestionSerializer(serializers.ModelSerializer):
class Meta:
user = serializers.ReadOnlyField()
model = QNAQuestion
fields = ('id','user','subject', 'body', 'solution')
So I think I have to modify my serializer. I tried HiddenInput and ReadOnly, but both don't really do the trick. If I make it hidden, than the the user_id is not required anymore, but it's also hidden when looking at existing objects. If I make it read only it's not required, but saving the serializer doesn't work anymore. I get the error message that the django object is not serializable to JSON.
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `user` attribute.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if not request.user.is_authenticated():
return False
if request.method in permissions.SAFE_METHODS:
return True
# Instance must have an attribute named `owner`.
return obj.user == request.user
So how can I get it fixed? The user_id should be visible, but I want it to be request.user and i don't want it to be required on creating new objects. Ideally it should also be hidden when using the auto generated api gui.
Thank you for your time. Sorry for spelling or grammar mistakes, I'm not a native speaker.
Try to make the field only required=False instead of Hidden or ReadOnly.
class QuestionSerializer(serializers.ModelSerializer):
class Meta:
model = QNAQuestion
fields = ('id','user','subject', 'body', 'solution')
extra_kwargs = {
'user': {'required': False}
}
Setup
I have a standard setup with model Account and corresponding AccountSerializer.
serializers.py:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = ('id', 'account_name', 'users', 'created')
views.py:
class SingleAccountView(generics.RetrieveAPIView):
serializer_class = AccountSerializer
queryset = Account.objects.all()
lookup_field = 'id'
permission_classes = ()
urls.py:
url(r'^account/(?P<id>\w+)$', SingleAccountView.as_view())
Goal
I want to be able to access the properties of my serializer by url, without hardcoding them into urls.py. E.g., I want to be able to go to website.com/account/23/account_name to get the account name for account 23. How can I achieve this?
You'll need to write a view explicitly to do that, as none of the generic views cover that use case.
Something along these lines would be about right...
class SingleAccountPropertyView(generics.GenericAPIView):
lookup_field = 'id'
def get(self, request, id, property_name):
instance = self.get_object()
if not hasattr(instance, property_name):
return Response({'errors': 'no such property'}, status.HTTP_404_NOT_FOUND)
return Response({'property': getattr(instance, property_name)}
You could also just use the regular APIView instead of GenericAPIView, in which case you'd want to write the instance lookup explicitly, instead of using the generic get_object()/lookup_field functionality that GenericAPIView provides.