POSTing to URL of one resource to create another, different resource - django-rest-framework

In my REST API I have two entities: Test and TestRun. I want to be able to send a POST request to create a TestRun (with the appropriate TestRun fields), but the URL of this request must be api/v1/test/{id}/start instead of api/v1/testrun.
I know that using #detail_route I can customise the URL, but then the request is still sent to api/v1/test/{id}:
class TestViewSet(viewsets.ModelViewSet):
queryset = Test.objects.all()
serializer_class = TestSerializer
#detail_route(methods=['post'], url_path='start')
def start_test(self, request, pk=None):
pass
class TestRunViewSet(viewsets.ModelViewSet):
queryset = TestRun.objects.all()
serializer_class = TestRunSerializer
Perhaps some highly customised router is needed here?

OK, I have the basic example. I think you have few problems, so first things first:
My views:
class TestViewSet(viewsets.ModelViewSet):
queryset = Test.objects.all()
serializer_class = TestSerializer
#detail_route(methods=['post'], url_path='start', serializer_class=TestRunSerializer)
def start_test(self, request, pk=None):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
# add here TestRun object
return Response(serializer.data, status=status.HTTP_200_OK)
class TestRunViewSet(viewsets.ModelViewSet):
queryset = TestRun.objects.all()
serializer_class = TestRunSerializer
My urls:
router = SimpleRouter()
router.register('test', TestViewSet)
router.register('test-run', TestRunViewSet)
urlpatterns = router.urls
and settings urls:
urlpatterns = [
url(r'^api/v1/', include('droute.urls'))
]
In this scenario you have full CRUD for Test and TestRun models - one is under api/vi/test and second in api/v1/test-run;
The detail_route decorator creates for you additional route: /api/v1/test/:id/start
But this do not mean that CRUD under api/v1/test-run is no longer accessible.
If you do not want to do not allow creation on api/v1/test-run you should use there ReadOnlyModelViewSet as a base for TestRunViewSet - this will allow only GET on the list endpoint: api/v1/test-run and on the details endpoint: api/v1/test-run//
You do not need to make magic in routers - as in example SimpleRouter is enough for that case.
Things are getting little bit more complicated if you want to make nested routers. You can search stackoverflow - there were many articles about that. But to be honest I would discourage you to use nested routers, I never feel that working with this is a pleasure :) You can check here:
https://github.com/alanjds/drf-nested-routers
I think (but I have little or nono information) that the best API for you would be something like this:
/api/v1/test -> CRUD for TEST
/api/v1/test/:id/start -> start the test POST
/api/v1/test/:id/runs -> get the runs list GET (list_route on TestViewSet or Nested router)
/api/v1/test/:id/runs/:run_id -> get the run details GET (and here is a problem - because it implies that you need nesting :(, or some custom view attached to the urls;)
Happy coding, hope this helps.

Related

Accessing ViewSet object list to provide extra context to serializer

I am attempting to add context to a serializer within a ModelViewSet which is dependent on the current paged object list in context. I'll explain with an example.
I am building a viewsets.ModelViewSet that lists Users and a list of favorite_foods. However- the list of user's favorite foods in some external microservice accessible via API. Building a ViewSet to collect objects and performing HTTP requests on each is trivial, we can do something like this:
class UserViewSet(viewsets.ViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserSerializer(serializers.ModelSerializer):
favorite_foods = serializers.SerializerMethodField()
def get_favorite_foods(self, instance):
# performs an HTTP call and returns a list[] of foods.
return evil_coupled_microservice_client.get_user_food_list(instance)
class Meta:
model = User
fields = ('id', 'name', 'favorite_foods')
The issue is (aside from some ugly infrastructure coupling) this is going to make an HTTP request count equivalent to the page size. Instead- it would be great if I could prefetch the favorite food lists for all users on the page in a single HTTP call, and just add them into context, like this:
class UserViewSet(viewsets.ViewSet):
def get_serializer_context(self):
context = super().get_serializer_context()
users = <-- This is where I want to know what users are in the current filtered, paginated response.
users_food_dict = evil_coupled_microservice_client.get_many_users_food_list(users)
context.update({'usesr_foods': users_food_dict})
return context
However- it doesn't appear there is any way to fetch the object list that's going to be serialized. Although (I'm fairly sure) get_serializer_context is called after the queryset is filtered and paginated, I'm not sure how to access it without doing some really hacking re-compiling of the queryset based on the query_params and other pieces attached to the class.
I'll post my current solution. It's not terrible, but I'm still hoping for a cleaner built-in.
class UserViewSet(viewsets.ViewSet):
...
def list(self, request, *args, **kwargs):
# Overrwite `ListModelMixin` and store current set
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
self.current_queryset = page
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
self.current_queryset = queryset
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
This is untested so far (not sure about functionality on Detail endpoints for instance) allows for the current_queryset to be fetched within the serializer context.

How to integrate/install drf-yasg to Django Rest project? Redoc/Swagger appears empty

I'm trying to integrate the drf-yasg to my Django Rest project. I installed the library via pip and added these code lines to the url.py as below.
schema_view = get_schema_view(
openapi.Info(
title="Costifier API",
default_version='v1',
description="Costifier API'ye hoşgeldiniz.",
terms_of_service="https://costifier.sfmyazilim.com",
contact=openapi.Contact(email="info#sfmyazilim.com"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path('', index),
path('admin/', admin.site.urls),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), #<-- Here
path('api/', include('sfmAPI.urls')),
]
One of my views is;
class PredictionView(views.APIView):
permission_classes = [AllowAny]
throttle_classes = [AnonymousUserThrottle]
queryset = Prediction.objects.all()
serializer_class = PredictionSerializer
def post(self, request, format=None):
serializer = PredictionSerializer(data=request.data)
if serializer.is_valid():
input_map_dict = json.loads(serializer.validated_data['input_map'])
username = serializer.validated_data['customer_name']
prediction_results = SmartRegression.smart_predict(username,
serializer.validated_data['model_name'],
input_map_dict,
isMember(username))
result = {
'inputs': serializer.data,
'error': '0',
'message': 'Successful',
'predicted_value': prediction_results[0],
'confidence': prediction_results[1],
'feature_importance': prediction_results[2]
}
return Response(status=status.HTTP_200_OK, data=result)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
My /redoc page is created. However, it has no content. It just has the APIView names such as below.
How can I fill the documentation?
The reason is you're using APIView instead of a generic view.
In the Django-Rest-Framework docs for schemas it mentions the below:
Note: The automatic introspection of components, and many operation parameters relies on the relevant attributes and methods of GenericAPIView: get_serializer(), pagination_class, filter_backends, etc. For basic APIView subclasses, default introspection is essentially limited to the URL kwarg path parameters for this reason.
A work around for this if you want to stick to using APIView instead of a generic view, is to use #swagger_auto_schema decorator (from drf-yasg) above each of your views. It's a bit of a pain but you should be able to find a few post on stackoverflow around using this. An example is:
DRF YASG CustomizingDRF YASG Customizing
Django-Rest-Framework does have a manual method of setting the parameters but most people would prefer to stick to the AutoSchema doing the work. I haven't been able to find much on how to use it for a beginner either. Hence drf-yasg seems a good way to go.
I believe for your post view above the below decorator should be what you're looking for just above the post method:
#swagger_auto_schema(
request_body=PredictionSerializer,
responses={
'200': 'OK Request',
'400': "Bad Request"
},
)
Also you want to add something like the below to the very top of your view since it will pull through as the description in the schema/documentations.
'''
def post(self, request, format=None):
"Description of your View"
'''
...

Require authorization for all but the listing view in a viewset

I have a ViewSet called BuildViewSet, with a bunch of detail views and one list view:
class BuildViewSet(viewsets.ModelViewSet):
queryset = Build.objects.all()
def list(self, request):
# Do some filtering on self.queryset based on user preferences
return super(BuildViewSet, self).list(request)
#detail_route(methods=['post'])
def transition(self, request):
…
# And a bunch of other methods, all prefixed with #detail_route
I set up REST Framework so the default authorization class is rest_framework.permissions.IsAuthenticated:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
However, I want my list() view to be available for everyone, even unauthenticated. I tried changing my list() method like this:
#list_route(permission_classes=(AllowAny,))
def list(self, request):
…
But this seems to have no effect:
AppError: Bad response: 401 UNAUTHORIZED (not 200 OK or 3xx redirect for http://localhost/api/v1/builds/)
'{"detail":"Authentication credentials were not provided."}
Changing the #detail_route to #permission_classes like this gives the same result:
#permission_classes((AllowAny,))
def list(self, request):
…
So it seems that list_route(…) is not the way to go, but in this case, what is?
You need to decorate the list method with the #permission_classes decorator, but the problem is that this decorator is only used for function-based views. Thus, you have two choices:
1) Convert the list view to a function-based view.
2) Authorize all views from the viewset by setting permission_classes = (AllowAny,) at the class level. In order to limit access to the other views, you will have to manually check the permissions using either a decorator or by calling the check_is_authenticated method:
def check_is_authenticated(self, request):
"""
Inspired from rest_framework.views.check_permissions
"""
if not IsAuthenticated.has_permission(request, self):
self.permission_denied(
request, message=getattr(permission, 'message', None)
)
Since all views that require a permission are already decorated with #detail_route, all you have to do is create a new #authenticated_detail_route decorator.
EDIT 3) Another, alternative solution would be to overload the check_permissions method:
def check_permissions(self, request):
if self.is_request_list(request):
return
return super(BuildViewSet, self).check_permissions(request)
The implementation of the is_request_list method is left as an exercise to the reader :-)
(seriously though, I'm not sufficiently familiar with django-rest-framework to offer an implementation. It would probably involve checking the request.method attribute)
EDIT As mentioned by the OP in a comment, in check_permissions the self.action attribute holds the "list" method name.

How to send parameter in AJAX (rest-api) using django?

I utilities rest-api in django,
and I don't succeed to send a "GET" parameter through ajax:
In rest-api app in django I have in the urls.py:
urlpatterns = patterns('',
url(r'^titles/(?P<author_id>\d+)/$', login_required(views.TitlesViewSet.as_view()) ),
)
In views.py I wrote:
class TitlesViewSetViewSet(ListCreateAPIView):
serializer_class = TitleSerializer
def get_queryset(self):
aouther_id = self.request.GET.get('aouther_id', None)
return Title.objects.filter(auther = auther_id)
when the code insert to the get_queryset above it doesn't recognize any GET parameter and the aouther_id is set to None.
Does anybody know what I should do?
First, you have a typo in urls, you are using author_id and in view you are trying to get the aouther_id key. Second, you are trying to get the value from the query parameters, but you are not actually using them. Third, you are using named url parameters and those are being stored in the kwargs property of your class based view.
You can access them this way:
class TitlesViewSetViewSet(ListCreateAPIView):
serializer_class = TitleSerializer
def get_queryset(self):
# try printing self.kwargs here, to see the contents
return Title.objects.filter(author_id=self.kwargs.get('author_id'))
you should replace a line of the auther_id setting to:
auther_id=self.kwargs['auther_id']
update:
I now see jbub answer... thanks man! I just discovered it...

get_queryset method and ViewSets in django rest framework

I am doing exactly as the example states
here is my method
class FeedViewSet(viewsets.ModelViewSet):
model = Feed
serializer_class = FullFeedSerializer
def get_queryset(self):
user = request.user
queryset = Feed.objects.get_nearby(user)
return queryset
when i execute it, it says request not defined .. which actually isn't. the example at the rest framework's site also haven't defined request. what am i doing wrong?
The request object is available (on either REST framework's class based views, or Django's standard class based views) as self.request. You're missing the self. part of that.

Resources