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

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"
'''
...

Related

DRF clear m2m field via patch with SlugRelatedField

Currently i'm using a SlugRelatedField in my ModelSerializer to represent a m2m-Field.
manager = serializers.SlugRelatedField(
many=True,
queryset=get_user_model().objects.all(),
slug_field='username',
required=False,
allow_null=True)
if i send now a patch request with e.g.
'manager': 'user1' or 'manager: ['user1', 'user2'] it works as expected.
but how can i achieve, that the m2m field gets cleared, when i send an empty array 'manager': []
at the moment nothing happens, when i do so.
If i try to clear it with 'manager': '' i get an "object with username= does not exist" error.
maybe the SlugRelatedField isn't the right choice for me? i just wanted to achieve handling users with their username instead of their pk.
Edit:
I can do a PUT Request with not sending 'manager' at all to clear it.
Is there maybe no way to clear a m2m field with a PATCH Request?
r = requests.patch('http://localhost:8000/api/projects/dfo2/',
auth = ('lala','lilu'),
data = {'developer': []}
)
result:
{'manager': ['peter', 'fllow'], 'description': '','name_short': 'dfo2', 'developer': ['peter'], 'name': 'asdf'}
what works is:
r = requests.patch('http://localhost:8000/api/projects/dfo2/',
auth = ('lala','lilu'),
data = '{"developer": []}',
headers = {'Content-type': 'application/json'}
)
but why does this not work:
r = requests.patch('http://localhost:8000/api/projects/dfo2/',
auth = ('lala','lilu'),
data = {"developer": []},
headers = {'Content-type': 'application/x-www-form-urlencoded'}
)
Without seeing your full code it is hard to know, but generally it seems like you are not dealing with the data properly. One piece of advice I always give people with DRF is to think about serializers as a tool for giving and validating data, but not necessarily describing what to do with it.
With that in mind, I suggest you try something like the following, although again it is hard without seeing the models and full serializer code:
class Projects(models.Model):
managers = models.ManyToManyField('auth.Users')
class ProjectSerializer(serializers.Serializer):
managers = serializers.SlugRelatedField(
many=True,
source='managers.all',
queryset=get_user_model().objects.all(),
slug_field='username',
required=False,
allow_null=True)
def update(self, instance, validated_data):
[instance.managers.add(manager) for manager in validated_data['managers']
return instance

POSTing to URL of one resource to create another, different resource

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.

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.

Django REST framework restrict posting and browsable Api-fields

I use the Django Rest framework together with an JavaScript app. I have some difficulties to get the posting of new data items right with the generic ModelViewSet
Most importantly I want to restrict what a poster can submit
(they should only be allowed to post items that have the user_id of this user (the authenticated user of the session).
I don't know when/where I should check for this? Is this a validation problem?
How I understand the permission classes is that they restrict the method (Post/Get) or check for user groups.
Also my user field in the item model is a foreign key to the user model
so the browsable api suggest in the Html-form a dropdown with the information about other users. (their email adresses and some other fields).
My data items look like this
[{
"id": 792,
"name": "test",
"category": 1,
"value": 5,
"user": "33"
}]
Here is my Serializer and the Viewset:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ('id',
'name',
'category',
'value',
'user',
)
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
I can't believe this issue with the DRF Create/Update (Post/Put) form isn't more widely discussed.
It's a huge data privacy issue - e.g. One can restrict the List API view to only show items owned by a User via overriding the get_queryset method inside as below:
# views.py
class ItemViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
But as OP notes, when accessing the API Create/Post or Update/Put form for the ItemViewSet, there is seemingly no easy way to restrict the user options to the user itself.
I had a similar issue myself building a survey platform, where I want to restrict choice of survey/question/options etc. to those owned by the user, and prevent users from inadvertently seeing each other's data.
Jocelyn's answer works for the OP's particular situation where we already know that the Item.user must equal request.user, so we override this on the perform_create method.
But Jocelyn's solution is insufficient for situations where you do not know in advance what the relationship between model instances will be (e.g. in my case where a new question objected could be added to any one of a user's surveys).
The solution I came up with was the nuclear option: do away with the Viewset altogether for Create and Update functionality, and use a set of custom views.APIView classes instead, as below (adapted for the case of the OP, only showing Create).
class ItemCreateView(views.APIView):
def post(self, request, format=None):
post_user_id = int(request.data['user'].split('/')[-2])
request_user_id = request.user.id
serializer = ItemSerializer(data=request.data, context={'request': request})
if post_user_id == request_user_id:
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)
else:
return Response('Not Allowed: Owner is not User', status=status.HTTP_401_UNAUTHORIZED)
Please note, I'm using a HyperlinkedModelSerializer rather than a plain ModelSerializer, hence the need for .split('/')[-2] to grab the post_user_id
Handling the user field
First set the user field to be readonly:
# serializers.py
class ItemSerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField()
class Meta:
model = Item
fields = ('id',
'name',
'category',
'value',
'user',
)
Then auto-set the user id on creation:
# views.py
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user.customer)
Handling permissions
Just use standard permissions mechanism to define a custom one :
# permissions.py
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return (request.user.is_authenticated() and
(obj.user == request.user.customer))
...and use it in your viewset :
# views.py
from permissions import IsOwner
class ItemViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwner]
...

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...

Resources