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.
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 have a viewset for stocks by ticker/company which corresponds to the urls. localhost:8000/stocks/AAPL works for example.
what I want is an extra action to route to localhost:8000/stocks/AAPL/income_statement, but I cant figure out how to create the extra action using the detail_route decorator below.
I've commented in the code base where I am having troubles
views.py
class StockViewSet(viewsets.ModelViewSet):
queryset = Stock.objects.all()
serializer_class = StockSerializer
# !!! this is what I don't know what to do !!!!
#detail_route(methods=["get"])
def get_is(self, request, statement):
stock = self.get_object()
serializer = IncomeStatementSerializer(data=request.data)
if serializer.is_valid():
return Response(serializer.data)
urls.py
router = DefaultRouter()
router.register(r"stocks", views.StockViewSet)
urlpatterns = router.urls
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 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
I want to be able to create or update an object using the same request. The operation should be idempotent.
Sending a PUT request to DRF work as expected if the object exists but if the object doesn't exists I get a 404 instead of creating it.
models.py:
class Btilog(models.Model):
md5hash = models.CharField(primary_key=True, max_length=32)
vteip = models.ForeignKey('vte.VTE')
timestamp = models.DateTimeField(blank=False)
source = models.TextField()
code = models.CharField(max_length=10, blank=False)
msg = models.TextField(blank=False)
api.py:
class BtilogSerializer(serializers.ModelSerializer):
class Meta:
model = models.Btilog
class BtilogVSet(viewsets.ModelViewSet):
queryset = models.Btilog.objects.all()
serializer_class = BtilogSerializer
permission_classes = (permissions.AllowAny,)
urls.py:
...
router = routers.DefaultRouter()
router.register(r'btilog', api.BtilogVSet)
urlpatterns = patterns('',
url(r'^api/', include(router.urls)),
...
)
Failing request
http --form PUT http://192.168.10.121:8888/logger/api/btilog/60c6b9e99c43c0bf4d8bc22d671169b1/ vteip='172.25.128.85' 'code'='Test' 'md5hash'='60c6b9e99c43c0bf4d8bc22d671169b1' 'timestamp'='2015-05-31T13:34:01' msg='Test' source='Test'
HTTP/1.0 404 NOT FOUND
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Type: application/json
Date: Mon, 09 Feb 2015 15:16:47 GMT
Server: WSGIServer/0.1 Python/2.7.6
Vary: Accept, Cookie
{
"detail": "Not found"
}
As described here: http://restcookbook.com/HTTP%20Methods/put-vs-post/ the correct behaviour of put should be to create the object if it doesn't exists.
The same error occurs using The Browsable API Tool from DRF to make the request. Is the behaviour of DRF also alike? What I'm doing wrong?
Well, maybe you should try to overwrite update method inside your modelviewset, which handle the PUT http method:
class BtilogVSet(viewsets.ModelViewSet):
queryset = models.Btilog.objects.all()
serializer_class = BtilogSerializer
permission_classes = (permissions.AllowAny,)
def update(self, request, *args, **kwargs):
try:
instance = Btilog.objects.get(pk=kwargs['pk'])
serializer = serializers.BtilogSerializer(instance=instance,data=request.data)
if serializer.is_valid():
btilog=serializer.save()
return Response(serializer.data,status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Btilog.DoesNotExist:
serializer = serializers.BtilogSerializer(data=request.data)
if serializer.is_valid():
btilog=serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Yes, in general with DRF you will create an object using a POST and update an object using PUT. Http PUTs should be idempotent, whereas POSTs are not necessarily so -and POSTs will never be idemponent if you have an automatically created field like a timestamp in the created object.
To get the effect the OP wishes above you need to place the create functionality of the POST http method into the PUT method.
The issue is that PUTs are mapped to the "update" action only (when using DefaultRouter in urls.py) and the update action does expect the object to exist. So you have to slightly amend the update function (from rest_framework.mixins.UpdateModelMixin) to handle creating objects that do not currently exist.
I am arriving somewhat late to this question so perhaps this may assist someone working on later versions of Django Rest Framework, my version is v3.9.4 .
if you are using a ModelViewSet, then I would suggest inserting the following update function within your views.py file, within your class viewset :
It is simply a blend of the DRF´s existing update and create mixins -and you get some extra checking thrown in with those mixins (permission checking, get_serializer_class etc.) Plus it is a bit more portable as it does not contain references to models, - well done to DRF developers (yet again). You will need to import Http404 and ValidationError as shown below.
from django.http import Http404
from rest_framework import status
from rest_framework.exceptions import ValidationError
class BtilogVSet(viewsets.ModelViewSet):
queryset = models.Btilog.objects.all()
serializer_class = BtilogSerializer
permission_classes = (permissions.AllowAny,)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
try:
instance = self.get_object() #throws a Http404 if instance not found
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
except Http404:
#create the object if it has not been found
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) # will throw ValidationError
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
except ValidationError: # typically serializer is not valid
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except:
raise
Note, PATCH is also mapped to the update() function indirectly via the function
partial_update().You don't need to include the partial_update code below, it is supplied by default from the file rest_framework.mixins.UpdateModelMixin, which is a mixin to the ModelViewSet. I show it here for purely illustrative purposes, you do not need to do anything to it.
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)