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.
Related
I am trying to create a single class based view for retrieving, listing, and creating all of my orders. I have gotten create and retrieve to work but listing is giving some problems. I am using DRF generic views to extend my view and have added the generics.ListApiView to my class. Once I added this however my retrieve route started acting unusual. It is returning to me a list of all the orders when I just want a specific one.
I tried to just add the generics.ListApiView to my class and override the list and get_queryset functions but that just started to affect my retrieve view.
class Order(generics.ListAPIView, generics.CreateAPIView, generics.RetrieveAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = addOrderSerializer
def get_queryset(self, **kwargs):
user = self.request.user
return Order.objects.filter(user=user)
def get_object(self):
pk = self.kwargs['pk']
obj = Order.objects.get(pk=pk)
return obj
def get_item_serializer(self, *args, **kwargs):
return addItemSerializer(*args, **kwargs)
def get_shipping_serializer(self, *args, **kwargs):
return addShippingSerializer(*args, **kwargs)
def create(self, request, *args, **kwargs):
user = request.user
data = request.data
orderItems = data.get('orderItems')
print(data)
if not bool(orderItems):
return Response('No Order Items', status=status.HTTP_400_BAD_REQUEST)
else:
# TODO - Create Order
orderSerializer = self.get_serializer(data=data)
orderSerializer.is_valid(raise_exception=True)
order = orderSerializer.save(user=user)
# TODO - Create Shipping Address
shippingSerializer = self.get_shipping_serializer(data=data)
shippingSerializer.is_valid(raise_exception=True)
shippingSerializer.save(order=order)
# TODO - Create Order Items and Set Order <> OrderItem Relationship
for item in orderItems:
product = Product.objects.get(pk=item['product'])
itemSerializer = self.get_item_serializer(data=item)
itemSerializer.is_valid(raise_exception=True)
item = itemSerializer.save(order=order, product=product)
# # TODO - Update Product CountInStock
product.countInStock -= item.qty
product.save()
return Response(data=orderSerializer.data)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
s = self.get_serializer(instance=instance)
return Response(data=s.data)
def list(self, request, *args, **kwargs):
qs = self.get_queryset()
s = self.get_serializer(qs, many=True)
return Response(s.data)
from django.contrib import admin
from django.urls import path, include
from .. import views
urlpatterns = [
path('', views.Order.as_view()),
path('add/', views.Order.as_view()),
path('<int:pk>/', views.Order.as_view({'get':'retrieve'})),
]
So in conclusion the list functionality of my view is working now but it has messed up my retrieve functionality. So that the retrieve function is only returning a list even tho I am adding the pk in my url.
Can you please post your urls.py as well for better clarity ?
Currently based on your question, the issue I see is mentioned below:
You are calling class "Order" for retrieve method with some url pattern eg path/int:pk -- Which is a GET request
You are also calling same class "Order" for list method with some url pattern eg path/ -- Which is a Get request
The issue is that both Retrieve and List Generic api has a GET method: snippet added :
Conclusion:
This is an example of Method Resolution Order in python inheritance.
Therefore, even though you are trying to invoke a GET method for retrieve it is envoking the GET method of LIST api because in your class definition
class Order(generics.ListAPIView, generics.CreateAPIView, generics.RetrieveAPIView) you have inherited ListAPIView first.
Solution :
You should separate out the classes eg: ListCreateAPIView, RetrieveUpdateDestroyAPIView
Alternatively :
You can also follow below stackoverflow answer to route GET request to specific method in same class :
Multiple get methods in same class in Django views
[EDIT]
The above suggested stackoverflow answer seems to be incorrect.
For multiple GET or POST request within same class, you can use Django Viewset and routers.
I found the below link to be well explained with examples:
https://testdriven.io/blog/drf-views-part-3/
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)
I'm trying to make a custom permission using this guide
views.py
class CustomModelList(generics.ListAPIView):
queryset = CustomModel.objects.all()
serializer_class = CustomModelSerializer
permission_classes = [IsAuthenticatedOrReadOnly, IsCustomOrReadOnly]
def get(self, request, format=None):
# some logic
def post(self, request, format=None):
# some logic
Just for experiment I've created this permission not to apply anyway
pesmissions.py
class IsCustomOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
return False
But when POST request sends to server it takes no effect -- I'm able to create new model instance.
I think that since you are using a list view, custom object level permissions are not checked automatically.
Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the filtering documentation for more details.
You can try overriding the has_permission method instead and see if that works, or check the permissions manually.
I have a list of all my objects when I use get method by api/movies in my api, and this is ok. I want also to get only one, specyfic object when use get method by api/movies/1 but now I still have a list of all my objects... What to change in my MoviesView or in urls?
My views.py:
class MoviesView(APIView):
def get(self, request):
movies = Movie.objects.all()
serializer = MovieSerializer(movies, many=True)
return Response(serializer.data)
My appurls.py:
urlpatterns = [
url('movies', MoviesView.as_view(), name="MoviesView"),
]
And my project urls.py:
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include("api.urls")),
]
When I use routers everythig crushes... Could you help me?
You can simply use viewsets.ModelViewSet that natively implements list and retrieve.
You declare something like router.register('movies', my_views.MoviesViewSet) in you urls.py and
class MoviesViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
permission_classes = [IsAuthenticated, ]
def get_queryset(self):
return self.queryset
def get_object(self):
movie_id = self.kwargs['pk']
return self.get_queryset().filter(id=movie_id)
def retrieve(self, request, *args, **kwargs):
try:
instance = self.get_object()
except (Movie.DoesNotExist, KeyError):
return Response({"error": "Requested Movie does not exist"}, status=status.HTTP_404_NOT_FOUND)
serializer = self.get_serializer(instance)
return Response(serializer.data)
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
This approach implies that you declare a Serializer, just like:
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
Django simply maps HOST/movies/ to list (multiple objects) and HOST/movies/PK/ to retrieve method (one single object).
Docs:
https://www.django-rest-framework.org/api-guide/viewsets/#modelviewset
https://www.django-rest-framework.org/api-guide/serializers/#modelserializer
Hope it helps.
BR.
Eduardo
I would suggest you if you want to retrieve just 1 element to use a Generic View, i.e RetrieveAPIView
It would give you all you need for getting 1 element.
from rest_framework import generics
class MoviesView(generics.RetrieveAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
but you need also to change urls.py
url(r'movies/(?P<pk>[0-9]+)/$', MoviesView.as_view(), name="MoviesView"),
When you make a GET request to "api/movies/1", the url is matched to the "api/movies" path (read more in the docs), and the MoviesView's get method is called. And your get() implementation just fetches all the movies (movies = Movie.objects.all()), serializes and returns them - that's why you get the entire list.
If you want to retrieve one specific object, you need to somehow specify which object you have in mind, using its primary key (in your case, id).
1. You have to define a separate path: movies/<int:pk>/ (btw, which Django version are you using? url has been deprecated, use path instead!)
2. You have to define a detail view to handle this new case, and pass it to the path function as the second argument.
This general problem can really be solved in many ways, and depending on your app you may want to use a ViewSet instead of views. Then you don't have to define paths (urls) separately - you can use a router. You can't use routers with your view, because router needs a viewset class as its argument.
If you provide more details, I could try to suggest something more specific.
My appurls.py:
use path method
urlpatterns = [
path('movies', MoviesView.as_view(), name="MoviesView"),]
Maybe it works
Start by adding a format keyword argument to both of the views, like so
def snippet_list(request, format=None):
and
def snippet_detail(request, pk, format=None):
Now update the snippets/urls.py file slightly, to append a set of format_suffix_patterns in addition to the existing URLs
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = [
path('snippets/', views.snippet_list),
path('snippets/<int:pk>', views.snippet_detail),
]
urlpatterns = format_suffix_patterns(urlpatterns)
I am using Django REST Framework to create an endpoint that will produce a PDF document. The PDF document will have information that corresponds to a particular Department. I have two desired functionalities -- to be able to download a PDF document, and to be able to preview the document within the browser.
Since the PDF document changes over time based on data that is added to the app, the document needs to be generated in real time when it is requested. As a first step, I'm trying to have the document be generated in a remote file storage location when the following endpoint is hit by a GET request:
departments/<department_pk>/result/preview
Since my endpoint should only take GET requests, I am using a ListAPIView. I'm trying to override the list method so that my custom document generation logic is executed, but it looks like the method is never called. How can I have some custom document generation logic be inserted into my endpoint, so that it is executed when the endpoint is hit by a GET request?
api/urls.py
url(r'^departments/(?P<department_pk>[0-9]+)/result/preview',
include(result_document_urls.result_document_preview_router.urls,
document_app/urls.py
result_document_preview_router = routers.DefaultRouter()
result_document_preview_router.register(r'^', ResultDocumentDetailView.as_view(),
base_name='Department')
document_app/views.py
class ResultDocumentDetailView(generics.ListAPIView):
queryset = Department.objects.all()
lookup_field = 'department_pk'
lookup_url_kwarg = 'department_pk'
def list(self, request, department_pk):
queryset = self.get_queryset()
import ipdb; ipdb.set_trace() # this break point is never hit
department = get_object_or_404(queryset, department_pk=department_pk)
...generate document logic...
return Response(status=status.HTTP_200_OK)
replace list method with below code, I think it will work
class ResultDocumentDetailView(generics.ListAPIView):
queryset = Department.objects.all()
lookup_field = 'department_pk'
lookup_url_kwarg = 'department_pk'
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
import ipdb; ipdb.set_trace() # this break point is never hit
department = get_object_or_404(
queryset, department_pk=kwargs.get('department_pk')
)
...generate document logic...
return Response(status=status.HTTP_200_OK)
for more reference see the overrinding method "list"
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py#L35
In your document_app/urls.py, you are incorrectly passing ResultDocumentDetailView as an argument instead of a viewset.
Router while registering accepts a ViewSet instead of an APIView.
There are two mandatory arguments to the register() method:
prefix - The URL prefix to use for this set of routes.
viewset - The viewset class.
Also, since you are only interested in the retrieve method, you can just create a ResultDocumentRetrieveView and add its corresponding url to your urls.py without the need of creating a ResultDocument router. (Routers are generally used when you want to handle both list and detail requests.)
class ResultDocumentRetrieveView(generics.RetrieveAPIView):
queryset = Department.objects.all()
lookup_field = 'department_pk'
lookup_url_kwarg = 'department_pk'
def retrieve(self, request, department_pk):
department = self.get_object()
...generate document logic...
return Response(status=status.HTTP_200_OK)
urls.py
url(r'^departments/(?P<department_pk>[0-9]+)/result/preview', ResultDocumentRetrieveView.as_view())