How to add suffix url in ModelViewSet
Serializer
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
def update(self, instance, validated_data):
...
...
ModelViewSet
I'm doing a custom partial update
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
http_method_names = ['get', 'patch', 'head', 'options']
def partial_update(self, request, *args, **kwargs):
super(CommentViewSet, self).partial_update(
request, *args, **kwargs)
return Response({
"data": request.data,
...
...
})
Urls
router = routers.DefaultRouter()
router.register(
"comments",
CommentViewSet
)
urlpatterns = [
path('api/v1/', include(router.urls))
]
Currently have this, but I want to add a suffix
url: http://localhost:8000/api/v1/comments/{id}
I want to do something like this
url: http://localhost:8000/api/v1/comments/{id}/update_or_whatever
What you want to do does not follow the REST architecture and popular practice. In REST, each endpoint represents a resource. The actions on the resource are represented by HTTP methods. So if you have the comments resource accessible through this url http://localhost:8000/api/v1/comments/, you can create (POST), get the list (GET) on the list endpoint and edit(PUT and PATCH), fetch a single comment (GET) and delete(DELETE) using the detail endpoint. In this way, you don't need to explicitly name the URL according to the action like http://localhost:8000/api/v1/comments/{id}/update. This is the architecture that DRF is built on and hence why you have this url style. Of course, there are actions like login and others that may not fit into this architecture and that's why DRF provides custom actions. But you should not use it to override the default actions mapped to HTTP methods
Another magic from DFR
https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions
Only change what u need in the view and add this action decorator.
In your views.py
#action(methods=['get'], detail=True, permission_classes=[IsAuthenticated])
def get_file(self, request, pk=None):
if pk is None:
raise ValueError("Found empty filename")
obj = self.get_queryset().filter(pk=pk).first()
if obj and obj.image_file:
return FileResponse(obj.image_file, content_type="image/jpeg")
return Response(
'Nothing to show',
status=status.HTTP_400_BAD_REQUEST)
Related
I'm using drf-yasg for swagger docs, but I'm using a BaseView which is the parent class view for all other views, thus all HTTP methods are written in BaseView so that children views don't need to implement these methods.
the problem is that when I added #swagger_auto_schema to baseView methods it didn't see the child attributes but the BaseView's attributes which are empty.
The BaseView:
class BaseView(APIView):
model = models.Model
serializer_class = serializers.Serializer
description = ''
id = openapi.Parameter('id', in_=openapi.IN_QUERY, type=openapi.FORMAT_UUID)
#swagger_auto_schema(manual_parameters=[id] ,responses={200: serializer_class},operation_description=description)
def get(self, request, id=None, **kwargs):
....
#swagger_auto_schema(request_body=serializer_class)
def post(self, request, **kwargs):
....
Then let's say I have this child view:
class TestApi(BaseView):
model = test
description = 'This is test api'
serializer_class = TestSerializer
In this approach Swagger is showing empty values for description and serializer_class because it doesn't see child values how to solve this without having to configure each view with HTTP methods and #swagger_auto_schema?
Thanks in advance.
Create a python file named api_docs.py and write a description of your API View of all methods.
docs = {
'View Name': {
'post': 'your description',
'list': '....description',
}
}
after that you have to import this into your view and add this.
from django.utils.decorators import method_decorator
#method_decorator(name="HTTP method name like post put destroy", decorator=swagger_auto_schema(
tags=["App name"], operation_description=docs['ViewName']['post']))
if you are overriding any method then you have to explicitly add on
that view like this
#swagger_auto_schema(tags=["App name"], operation_description=docs['View Name']['post'])
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
I have this code of 2 views in Django. You will notice that each REST API call has a verify_login() function call that ensures that the request contains a verified JWT token. I'm wondering if there's a better way to implement this so that I don't have to have these lines specifically in every REST endpoint
verify_response = verify_login(request)
if verify_response not None:
return verify_response
I'm trying to follow the D.R.Y. (Do Not Repeat Yourself) principle of coding. It'd be nice if there was a cleaner way to represent this. I thought about maybe creating a module extending APIView that automatically has this and then all my Views extend that, but runs into the issue of having to call super().API_REQUEST() and then having to do the same if-statement check to see if it's None or not.
class PostView(APIView):
"""
View for Post object
* requires token authentication
"""
# Create post
#swagger_auto_schema(
request_body=PostSerializer,
operation_description="Create a post object"
)
def post(self, request):
verify_response = verify_login(request)
if verify_response not None:
return verify_response
serializer = PostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# get all posts
#swagger_auto_schema(
operation_description="Get all posts from the DB"
)
def get(self, request):
verify_response = verify_login(request)
if verify_response not None:
return verify_response
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
You can use authentication classes alongside permission classes. If you want the authentication check to happen for all the APIs of your application, put your classes in settings.REST_FRAMEWORK. If you want it for specific APIView's, put them in permission_classes & authentication_classes class variables. Check out the doc for more details.
Example,
class JWTAuthenticataion(BaseAuthentication):
def authenticate(self, request):
... # write your JWT implementation here
# settings.py:
REST_FRAMEWORK = {
...
"DEFAULT_AUTHENTICATION_CLASSES": (
"path.to.JWTAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
)
}
# or
# api.py
class YourAPIView(APIView):
permission_classes = (IsAuthenticated, )
authentication_classes = (JWTAuthentication, )
...
I made a view that can use put, delete request using modelviewset and mapped it with url. I have clearly made it possible to request put and delete request to url, but if you send delete request to url, I return 405 error. What's wrong with my code? Here's my code.
views.py
class UpdateDeletePostView (ModelViewSet) :
serializer_class = PostSerializer
permission_classes = [IsAuthenticated, IsOwner]
queryset = Post.objects.all()
def update (self, request, *args, **kwargs) :
super().update(request, *args, **kwargs)
return Response({'success': '게시물이 수정 되었습니다.'}, status=200)
def destroy (self, request, *args, **kwargs) :
super().destroy(request, *args, **kwargs)
return Response({'success': '게시물이 삭제 되었습니다.'}, status=200)
feed\urls.py
path('post/<int:pk>', UpdateDeletePostView.as_view({'put': 'update', 'delete': 'destroy'})),
server\urls.py
path('feed/', include('feed.urls')),
and error
"detail": "method \delete\ not allowed"
as I wrote in the comment looks like you don't need a ViewSet because you are handling just operations on a single item.
In general you can restrict the operations available for Views or ViewSet using proper mixins.
I suggest two possible approaches
Use Generic View
class UpdateDeletePostView(
UpdateModelMixin,
DeleteModelMixin,
GenericAPIView):
.....
and
urlpatterns = [
path('post/<int:pk>', UpdateDeletePostView.as_view()),
...
]
Use ViewSet and Router
class UpdateDeletePostViewSet(
UpdateModelMixin,
DeleteModelMixin,
GenericViewset):
.....
router = SimpleRouter()
router.register('feed', UpdateDeletePostViewSet)
urlpatterns = [
path('', include(router.urls)),
...
]
TLDR;
Use the detail-item endpoint instead of the list-items endpoint.
http://127.0.0.1:8000/api/items/<item-id>/
instead of
http://127.0.0.1:8000/api/items/
This happened with a project I was working on. Turns out I was trying to PUT, PATCH, DELETE from the list-items endpoint rather than the detail-item endpoint.
If you implemented your viewset using the ModelViewSet class, and your GET and POST methods work. Chances are you may be using the wrong endpoint.
Example:
If you're trying to retrieve all your products from you use the list-items endpoint which will look something like:
http://127.0.0.1:8000/api/product/
If you're trying to get the detail of a specific item, you use the detail-item endpoint which looks like:
http://127.0.0.1:8000/api/product/2/
Where 2 is the id of the specific product that you're trying to PUT, PATCH, or DELETE. The reason you get the 405 is because it is your fault (not the servers fault), that you're applying item-level methods to an endpoint that provides lists of items - making your request ambiguous.
How can I make a ModelViewSet accept the POST method to create an object? When I attempt to call the endpoint I get a 405 'Method "POST" not allowed.'.
Within views.py:
class AccountViewSet(viewsets.ModelViewSet):
"""An Account ModelViewSet."""
model = Account
serializer_class = AccountSerializer
queryset = Account.objects.all().order_by('name')
Within serializers.py:
class AccountSerializer(serializers.ModelSerializer):
name = serializers.CharField(required=False)
active_until = serializers.DateTimeField()
class Meta:
model = Account
fields = [
'name',
'active_until',
]
def create(self, validated_data):
with transaction.atomic():
Account.objects.create(**validated_data)
within urls.py:
from rest_framework import routers
router = routers.SimpleRouter()
router.register(
prefix=r'v1/auth/accounts',
viewset=AccountViewSet,
base_name='accounts',
)
Do I need to create a specific #action? my attempts to do so have yet to be successful. If that is the case what would the url = reverse('app:accounts-<NAME>') be such that I can call it from tests? I haven't found a full example (urls.py, views.py, serializers.py, and tests etc).
I discovered what the issue was, I had a conflicting route. There was a higher level endpoint registered before the AccountViewSet.
router.register(
prefix=r'v1/auth',
viewset=UserViewSet,
base_name='users',
)
router.register(
prefix=r'v1/auth/accounts',
viewset=AccountViewSet,
base_name='accounts',
)
Django runs through each URL pattern, in order, and stops at the first one that matches the requested URL.. I should have been ordered this way:
router.register(
prefix=r'v1/auth/accounts',
viewset=AccountViewSet,
base_name='accounts',
)
router.register(
prefix=r'v1/auth',
viewset=UserViewSet,
base_name='users',
)
despite the fact that reverse('appname:acccounts-list') worked, the underlying URL router still thought I was calling the UserViewSet.
From the docs:
A ViewSet class is simply a type of class-based View, that does not provide any method handlers such as .get() or .post(), and instead provides actions such as .list() and .create().
And here is a list of supported actions:
def list(self, request):
pass
def create(self, request):
pass
def retrieve(self, request, pk=None):
pass
def update(self, request, pk=None):
pass
def partial_update(self, request, pk=None):
pass
def destroy(self, request, pk=None):
pass
So no post is not directly supported but create is.
So your end point would be v1/auth/accounts/create when using the a router instead v1/auth/accounts/post.
I honestly prefer using class based or function based views when working with DRF. It resembles regular django views more closely and makes more sense to me when working with them. You woul write you views and urls pretty much like regular django urls and views.
In DRF's DefaultRouter url router, it requires a {lookup} parameter to route DELETE requests to the destroy method of a ModelViewSet (so, you'd make your request to delete an object instance to the endpoint {prefix}/{lookup}/).
This is fine for deleting a single instance, but I'd like to extend that functionality to deleting multiple instances on a single request. Let's say the lookup parameter is called uuid and the model is called Product. Here's an extended version of destroy:
def destroy(self, request, uuid=None):
"""
Overridden method allows either url parameter of single UUID
(to delete a single instance), or multiple query parameters `uuids`
to delete multiple instances.
"""
if not uuid:
uuids = request.query_params.get('uuids', None)
if not uuids:
return Response(status=status.HTTP_404_NOT_FOUND)
if len(uuids) != Product.objects.filter(uuid__in=uuids).count():
return Response(status=status.HTTP_404_NOT_FOUND)
Product.objects.filter(uuid__in=uuids).delete()
else:
instance = self.get_object(uuid)
if not instance:
return Response(status=status.HTTP_404_NOT_FOUND)
instance.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
So this version takes a DELETE request and multiple uuids[] query parameters in the url. Now I just need to route it in urls.py:
from rest_framework.routers import DefaultRouter, Route
class BulkDeleteRouter(DefaultRouter):
"""
a custom URL router for the Product API that correctly routes
DELETE requests with multiple query parameters.
"""
def __init__(self, *args, **kwargs):
super(BulkDeleteRouter, self).__init__(*args, **kwargs)
self.routes += [
Route(
url=r'^{prefix}{trailing_slash}$',
mapping={'delete': 'destroy'},
name='{basename}-delete',
initkwargs={'suffix': 'Delete'}
),
]
bulk_delete_router = BulkDeleteRouter()
bulk_delete_router.register(r'product', ProductViewSet, base_name='product')
This, unfortunately, has killed my url router. It won't resolve GET to the appropriate methods in the viewset, and I don't understand why - isn't my BulkDeleteRouter supposed to extend this functionality from the DefaultRouter? What did I do wrong?
Forgot to add the router urls to the urlpatterns. I must be blind.
urlpatterns += [
url(r'^API/', include(bulk_delete_router.urls, namespace='api')),
]
Adding an additional 'delete': 'destroy' to the 'List route' route will perfectly do the job.
class CustomRouter(DefaultRouter):
"""
a custom URL router for the Product API that correctly routes
DELETE requests with multiple query parameters.
"""
routes = [
# List route.
Route(
url=r'^{prefix}{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create',
'delete': 'destroy', # The magic
},
name='{basename}-list',
detail=False,
initkwargs={'suffix': 'List'}
),
# Dynamically generated list routes. Generated using
# #action(detail=False) decorator on methods of the viewset.
DynamicRoute(
url=r'^{prefix}/{url_path}{trailing_slash}$',
name='{basename}-{url_name}',
detail=False,
initkwargs={}
),
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
mapping={
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
},
name='{basename}-detail',
detail=True,
initkwargs={'suffix': 'Instance'}
),
# Dynamically generated detail routes. Generated using
# #action(detail=True) decorator on methods of the viewset.
DynamicRoute(
url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
name='{basename}-{url_name}',
detail=True,
initkwargs={}
),
]
Then use the router like this:
custom_router = CustomRouter()
custom_router.register(r'your-endpoint', YourViewSet)
urlpatterns = [
url(r'^', include(custom_router.urls)),
]
The viewset:
from rest_framework import viewsets, status
from rest_framework.response import Response
from django.db.models import QuerySet
class MachineSegmentAnnotationViewSet(viewsets.ModelViewSet):
def destroy(self, request, *args, **kwargs):
qs: QuerySet = self.get_queryset(*args, **kwargs)
qs.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Hope this helps.