I'm implementing 'users/me/'. (I alse read the the article, but I'm try to add some specific function)
I made a function in UserViewSet:
#list_route()
def me(self, request):
# ... find user ...
request.path = reverse('user-detail', kwargs={'pk': user.id})
self.partial_update(request)
It raises AssertionError: Expected view UserViewSet to be called with a URL keyword argument named "pk". Fix your URL conf, or set the `.lookup_field` attribute on the view correctly.
print(request.path) is /users/2/, so reverse is working.
urls.py:
router = routers.DefaultRouter()
router.register(r'users', UserViewSet, base_name='user')
How could I deliver a pk? I have no idea what is the mistake I made.
UserViewSet code:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
#list_route(methods=['patch'], permission_classes=[IsAuthenticated])
def me(self, request):
user = get_object_or_404(Token, key=request.auth).user
request.path = reverse('user-detail', kwargs={'pk': user.id})
self.partial_update(request, pk=user.id)
serializer = self.get_serializer(user)
return Response(serializer.data)
I would not implement it using a list_route but rather using another approach: overriding get_object(self) on the view:
def get_object(self):
if self.kwargs.get(self.lookup_field, None) == 'me':
# ... find user probably using self.get_queryset() or return self.request.user
self.check_object_permissions(self.request, user) # may not be required, see get_object link
return user
return super().get_object()
This make you API react exactly the same whenever you use 'me' or the user pk and actions will be the same (get, put, patch, ... detailed_routes)
Notes:
this only works if you are using a GenericView or subclass like ModelViewset
I use python3 super syntax, do not forget to adapt it if you are using python2
Related
I Created a view for article model and I have the data in my database but when I go on url then it's showing empty array
my below code
view.py
class ArticleView(RetrieveAPIView, RetrieveUpdateAPIView, RetrieveDestroyAPIView):
serializer_class = ArticleSerializer
permission_classes = [AllowAny]
pagination_class = ArticleSizeLimitPagination
def get_queryset(self):
return Article.objects.filter().order_by('-updated_on')
def get_object(self):
try:
return Article.objects.get(id=self.request.data['id'], is_deleted=False)
except Exception:
return None
def perform_create(self, serializer):
return serializer.save(user=self.request.user)
def destroy(self, request, *args, **kwargs):
try:
instance = self.get_object()
instance.is_deleted = True
instance.save()
return Response(data='delete success')
except Http404:
return Response(status=status.HTTP_204_NO_CONTENT)
urls.py
urlpatterns = [
path('', ArticleView.as_view()),
path('<uuid:pk>/', ArticleView.as_view()),
]
When I run the url then
HTTP 200 OK
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"sub_title": "",
"upvote": null,
"title": ""
}
Where did I do wrong? Is there issue in def function for RetrieveAPIView?
You created a view with selected mixins. You will be able to get one instance, update or delete. Is this what you want? Here you have the list of available mixins: DRF docs.
When you open the URL in the browser you probably get the DRF browsable API. It will show you nice forms to interact with your endpoints. If you want to get the article from the endpoint, you should provide URL with id (pk (where pk is primary key)).
Your URLs looks wrong. Please try:
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(
r"articles",
ArticleView
)
urlpatterns = [
url(r"/", include(router.urls)),
]
I would recommend you to use ModelViewSet instead of building view from mixins. It should be easier.
class ArticleView(ModelViewSet):
Please make it works with ModelViewSet (easier solution) and then try to narrow down with a harder solution, by defining mixins.
I am trying to implement an api version of a play button on a django website.
This is how far I got:
models.py
class Note(models.Model):
plays = models.ManyToManyField(settings.AUTH_USER_MODEL,blank=True,related_name='track_plays')
def get_play_url(self):
return "/play/{}/play".format(self.pk)
def get_api_like_url(self):
return "/play/{}/play-api-toggle".format(self.pk)
views.py
class TrackPlayToggle(RedirectView):
def get_redirect_url(self,*args,**kwargs):
id = self.kwargs.get("id")
obj = get_object_or_404(Note,id=id)
url_ = obj.get_absolute_url()
user = self.request.user
if user.is_authenticated():
if user in obj.plays.all():
obj.plays.add(user)
else:
obj.plays.add(user)
return url_
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication,permissions
from rest_framework.decorators import api_view
class TrackPlayAPIToggle(RedirectView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
#api_view(['GET', 'POST', ])
def get(self,request,format=None):
obj = get_object_or_404(Note,id=id)
url_ = obj.get_absolute_url()
user = self.request.user
updated = False
played = False
if user.is_authenticated():
if user in obj.plays.all():
played = True
obj.plays.add(user)
else:
played = True
obj.plays.add(user)
played = False
updated = True
data = {
"updated":updated,
"played":played
}
return Response(data)
urls.py
url(r'^(?P<id>\d+)/play/', TrackPlayToggle.as_view(), name='play-toggle'),
url(r'^api/(?P<id>\d+)/play/', TrackPlayAPIToggle.as_view(), name='play-api-toggle'),
Ive added the API Decorator, because without it, I get a TypeError:
get() got an unexpected keyword argument 'id'
and when I try to add id=None I get an AssertionError:
.accepted_renderer not set on Response
Is this because I used id instead of slug?
Thank you for any suggestions
I don't understand why you thought adding the #api_view decorator would solve your TypeError. That decorator is for function-based views; it has no use in class-based views, where you define which methods are supported by simply defining the relevant methods. Remove the decorator.
The way to solve the original problem is to add the id parameter to the method; and the way to solve the problem with the renderer is to inherit from the correct parent class, which should clearly not be RedirectView.
class TrackPlayAPIToggle(GenericAPIView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, id, format=None):
...
I'm forced to write some messy work, I need to create partial_update view, however it must use POST method, because the software in the end does not use PUT/PATCH methods.
Here are some assumptions:
everything needs to be routed via DefaultRouter() in urls.py - that's
why I'm using GenericViewSet
must use POST method for updating one field - that's why I'm overwriting post() method of UpdateModelMixin
instance.visible is a Boolean, which state is set to True as soon as the body is not empty.
Update works, except the permission_classess which are ignored. It totally does not check the request for valid credentials. I think it's because I totally overwrote the post(), right? How do I force authentication check within the post method?
urls.py:
from django.urls import include, path
from rest_framework import routers
from browse.views import *
router = routers.DefaultRouter()
[...]
router.register(r'update-article', UpdateArticleBodyViewSet)
urlpatterns = [
path('api/', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'),)
]
views.py:
class UpdateArticleBodyViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticated, )
def post(self, request, pk):
instance = get_object_or_404(Article, pk=pk)
instance.body = request.data.get("body")
if instance.body:
instance.visible = True
instance.save()
serializer = self.get_serializer(instance=instance, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
UPDATE
I've changed the code after the first question, now it looks like this:
views.py:
class UpdateArticleBodyViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticated, )
def partial_update(self, request, *args, **kwargs):
instance = self.queryset.get(pk=kwargs.get('pk'))
instance.body = request.data.get("body")
if instance.body:
instance.visible = True
instance.save()
serializer = self.get_serializer(instance=instance, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
urls.py
articles_viewset = UpdateArticleBodyViewSet.as_view({
'post': 'update'
})
router = routers.DefaultRouter()
router.register(r'update-article', articles_viewset, basename="article")
Which results in following error:
AttributeError: 'function' object has no attribute 'get_extra_actions'
There is a couple questions about it on StackOverflow already, but none of them provide the answer. Is there any way I can use Router in this case or am I forced to write urls explicitly?
The problem here is that both classes you are inheriting from does not have a post method, so you are not actually overing it. So the method is out of the authentication scope.
There are a lot of different ways to accomplish this. The most simple way I can think of is to change the post action for your route. Something like:
articles_viewset = UpdateArticleBodyViewSet.as_view({
'post': 'update'
})
router.register(r'update-article', articles_viewset)
This way, you would be able to use UpdateMixin without any problem. You sould just to tweak the update method if necessary.
I know a lot of people already asked about handling with current user but I couldn't find solution so I post this.
What I want to do is to get, put and delete current user without providing pk.
I want to set endpoint like users/my_account
My current code is here
class MyAccountDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = CustomUser.objects.all()
serializer_class = UserSerializer
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
And now I can get current user's info but when I try to update or delete the current user,
AssertionError: Expected view MyAccountDetail to be called with a URL keyword argument named "pk". Fix your URL conf, or set the .lookup_field attribute on the view correctly.
How can I solve this?
Update
urlpatterns = [
path('users/my_account', views.MyAccountDetail.as_view()),
]
In this case, you will need to override get_object() method in your MyAccountDetail view. For example:
from rest_framework.permissions import IsAuthenticated
class MyAccountDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = CustomUser.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return self.request.user
You need to do that, because by default get_object method looks for lookup_url_kwarg or lookup_field in the URL, and from that it will try to fetch the object using pk or whatever you have configured in lookup_field or lookup_url_kwarg.
FYI, I have added a permission class as well, because without it, self.request.user will be an anonymous user, hence will throw error.
So here is a function in my views.py:
#api_view(['GET'])
#csrf_exempt
#authentication_classes(BasicAuthentication)
#permission_classes((IsAuthenticated,))
def UserAPI(request):
userlist = MyProfile.objects.all()
serializer = UserSerializer(userlist, many=True)
return Response(serializer.data)
Now if I go to this URL, it shows me all the user information despite not being logged in. I'm not sure what I'm doing wrong? I'm assuming I don't need to set DEFAULT_AUTHENTICATION_CLASSES in my settings since I'm using the decorators.
I think there's a syntax error there: for the authentication_classes you should send an iterable, not a single element. Could you try with the following code:
#api_view(['GET'])
# #csrf_exempt # you should be safe without this on a GET method
#authentication_classes((BasicAuthentication, ))
#permission_classes((IsAuthenticated, ))
def UserAPI(request):
userlist = MyProfile.objects.all()
serializer = UserSerializer(userlist, many=True)
return Response(serializer.data)