Accessing serializer fields by url in Django Rest Framework - django

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.

Related

Filtering querysets in Django Rest Framework

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.

Why my get_queryset doesn't work when used on Model with OneToOneField

Hello Django Programmers,
I have an issue which I don't understand.
Here is my model class:
class Profile(models.Model):
name = models.CharField(max_length=50)
employeeView = models.BooleanField(default=True)
owner = models.OneToOneField(User, related_name="profile", on_delete=models.CASCADE, null=True)
This class extends my User Model. Please notice that I'm using here OneToOneField in relation to User Model.
This model is serialized with that Serializer:
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Profile
fields = (
'url',
'pk',
'name',
'employeeView')
And finally I have view which I use to process GET (list) and POST requests to the database:
below view works fine (for GET request), but it gives me all Profiles related with all users
class ProfileList(generics.ListCreateAPIView):
permission_classes = [
permissions.IsAuthenticated
]
serializer_class = ProfileSerializer
name = 'profile-list'
queryset = Profile.objects.all()
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
Because I would like to create view which could give me only profile for My user, I modifiet above view like so:
class ProfileList(generics.ListCreateAPIView):
permission_classes = [
permissions.IsAuthenticated
]
serializer_class = ProfileSerializer
name = 'profile-list'
##queryset = Profile.objects.all()
def get_queryset(self):
return self.request.user.profile.all()
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
But when I send the GET request I get this error:
{code}
'Profile' object has no attribute 'all'
{code}
So my question here is more general. Above view will work when I will use in my Profile model ForeignKey field instead of OneToOneField. In such case when in my view I will request for all objects which belongs to my user like so:
def get_queryset(self):
return self.request.user.profile.all()
I will get necessary data. But how to ask for objects when we have OneToOneField? It seems that the procedure is different, but I can not find it anywhere.
all() implies that there are many related objects, so this is applicable for reverse one-to-many (ForeignKey) and many-to-many (ManyToManyField) calls.
In case of one-to-one (OneToOneField), as the name suggests, one object can be related to only one of the object of the related model, hence there's no point of all-like methods here.
Now, the get_queryset method is supposed to return a queryset of objects, not a single one. So you can not use self.request.user.profile as that refers to only one instance.
You need to return a queryset with the only instance of Profile that is related to the requesting User:
def get_queryset(self):
return Profile.objects.filter(owner=self.request.user)
The documentation https://docs.djangoproject.com/en/2.2/topics/db/examples/one_to_one/ says that you would access the user profile by using user.profile. This makes sense, since it’s a OneToOneField and so we shouldn’t expect a QuerySet, just a single object.

Django Rest Framework check_object_permissions not being called

I am trying to make sure the user has permission to view the object they are calling. Here is my permissions class:
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to do actions.
"""
message = 'You must be the owner of this object.'
def has_object_permission(self, request, view, obj):
print("CHECK THAT I GOT HERE")
return obj.user == request.user
And here is my ViewSet:
class TopLevelJobViewSet(ModelViewSet):
permission_classes = (IsOwner,)
serializer_class = TopLevelJobSerializer
queryset = TopLevelJob.objects.all()
filter_backends = [DjangoFilterBackend, RelatedOrderingFilter]
filter_class = TopLevelJobFilter
ordering_fields = '__all__'
Thehas_object_permissions is not being called, anyone visiting the endpoint is able to access all the objects.
Why is this? How do I get has_object_permissions to get called?
This post: Django rest framework ignores has_object_permission talks about it being an issue with not having GenericAPIView. But ModelViewSet has GenericViewSet which has generics.GenericAPIView. Is something overriding this somewhere?
EDIT: My issue was that I was calling list instead of get. How can I only returns objects in list that belong to a user?
This link: https://www.django-rest-framework.org/api-guide/filtering/#filtering-against-the-current-user shows I could implement something like this:
def get_queryset(self):
username = self.kwargs['username']
return Purchase.objects.filter(purchaser__username=username)
This seems to violate DRY if I have to add this to every viewset. Is there a way to turn this into a permissions class that I could always call?
You can implement custom generic filtering [drf-doc]. For example:
class IsOwnerFilter(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
return queryset.objects.filter(user=request.user)
Then you can add this to your ModelViewSet:
class TopLevelJobViewSet(ModelViewSet):
permission_classes = (IsOwner,)
serializer_class = TopLevelJobSerializer
queryset = TopLevelJob.objects.all()
filter_backends = [IsOwnerFilter, DjangoFilterBackend, RelatedOrderingFilter]
filter_class = TopLevelJobFilter
ordering_fields = '__all__'

How to covert two ListAPIViews that are different into a single ModelViewSet

I have a django project I am working on that needs a users and account model. I am using and integrating Django Rest Framework. I was initially using individual API generic views from DRF. I am thinking of converting the individual generic views into a view set. I was able to do it for the user model. I wan to convert the Account model views to a view set.
My issue is that I have two versions of the same ListAPIView for the profile model. The top View lists all of the accounts in the database and the second one list all the accounts for an idividual user based on the User__Username foreignkey in the Account model.
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.all()
lookup_field = 'username'
class AccountListView(ListAPIView):
queryset = Account.objects.all()
serializer_class = AccountSerializer
class AccountUserListView(ListAPIView):
queryset = Account.objects.all()
serializer_class = AccountSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ('user', '_id', '_class')
def get_queryset(self):
return self.queryset.filter(user_username=self.kwargs.get('username'))
It says that I can specifically define the properties of a view within the viewset but I want to define two versions of the ListAPIView for the single model. Is there a way to double define the same view in a single viewset.
I basically want to define both of my Account ListAPIViews in the same viewset. How should i go about doing that if it is possible??
you can use the #action decorator to define.
class UserViewSet( mixins.ListModelMixin, viewsets.GenericViewSet):
"""
A simple ViewSet for listing or retrieving users.
"""
def list(self, request):
queryset = User.objects.all()
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
#acttion(method="get", details=False, serializer_class=AccountSerializer)
def account_list(self, request, *args, **kwargs):
queryset = User.objects.all()
data = self.get_serializer_class().(instance=queryset, many=True).data
return response.Response(data)

Use serializer validated data in get_queryset()

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.