Custom Grouping on OpenAPI endpoints with Django Rest Framework - django

I have a Django project and I am using Django REST framework. I am using drf-spectacular
for OpenAPI representation, but I think my problem is not tied to this package, it's seems a more generic OpenAPI thing to me (but not 100% sure if I am right to this).
Assume that I have a URL structure like this:
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include([
path('v1/', include([
path('auth/', include('rest_framework.urls', namespace='rest_framework')),
path('jwt-auth/token/obtain', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('jwt-auth/token/refresh', CustomTokenRefreshView.as_view(), name='token_refresh'),
path('home/', include("home.urls"))
]))
])),
# OpenAPI endpoints
path('swagger/', SpectacularSwaggerView.as_view(url_name='schema-swagger-json'), name='schema-swagger-ui'),
path('swagger.yaml/', SpectacularAPIView.as_view(), name='schema-swagger-yaml'),
path('swagger.json/', SpectacularJSONAPIView.as_view(), name='schema-swagger-json'),
path('redoc/', SpectacularRedocView.as_view(url_name='schema-swagger-yaml'), name='schema-redoc'),
]
In the corresponding swagger UI view, I get all endpoints grouped under api endpoint, e.g.:
If add more endpoints under v1, all go under the api endpoint.
What I want to achieve is, to have the endpoints in Swagger grouped differently, e.g. by app. So I'd have home, jwt, another_endpoint, instead of just api, so it will be easier to navigate in Swagger (when I add more endpoints, with the current method it's just showing a massive list of URLs, not very user friendly).
I've read that those groups are being extracted from the first path of a URL, in my case this is api, so if I change the URL structure, I could achieve what I need.
But isn't there another way of doing this? I want to keep my URL structure, and customize how I display this with OpenAPI, so in the end I have a swagger that groups the endpoints by app, so it's easier to navigate and find what you are looking for.

you are making it harder than it needs to be. In the global settings you can specify a common prefix regex that strips the unwanted parts. that would clean up both operation_id and tags for you. In your case that would probably be:
SPECTACULAR_SETTINGS = {
'SCHEMA_PATH_PREFIX': r'/api/v[0-9]',
}
that should result in tags: home, jwt-auth, swagger.json, swagger.yaml
the tags on #extend_schema is merely a convenience to deviate from the default where needed. it would be cumbersome to do this for every operation. see the settings for more details:
https://drf-spectacular.readthedocs.io/en/latest/settings.html
for even more elaborate tagging you can always subclass AutoSchema and override get_tags(self) to your liking. cheers!

Turns out that you can control this by changing the tags in a view, as per OpenAPI specification: https://swagger.io/docs/specification/grouping-operations-with-tags/
So, with drf-spectacular, you can use the extend_schema decorator to achieve this, e.g.:
from drf_spectacular.utils import extend_schema
class CustomTokenObtainPairView(TokenObtainPairView):
"""
Takes a set of user credentials and returns an access and refresh JSON web
token pair to prove the authentication of those credentials.
"""
#extend_schema(
operation_id="jwt_obtain",
....
tags=["aTestTag"]
)
def post(self, request, *args, **kwargs):
# whatever
So you have to use this decorator to extend the schema in each view that you want to put into a custom group.

Related

Set up DRF Browsable API Root with all api urls

In my urls.py I have many rest framework urls:
path(
"api/",
include([
path("users/", api_views.users, name="users"),
path("proposals/", api_views.Proposals.as_view(), name="proposals"),
path("requests/", api_views.Requests.as_view(), name="requests"),
#...
])
)
I can visit the individual endpoints in the browser and access the browsable API, but I want to set up a browsable API Root where I can see all the available endpoints. The DRF tutorial has an example in which they list the urls they want in the root, but I have a lot of urls and I want all of them to show up. Is there a way to include all the urls in my "api/" path?
You should try to use drf_yasg package. Here's a documentation for that.

How to set Authentication and Permission on django URLs for imported views

I want to use django REST framework's inbuilt API documentation. The problem is that I need it to be private with a login and I couldn't manage thus far. I do the following to create my API doc as documented:
from rest_framework.documentation import include_docs_urls
API_TITLE = "Cool title"
API_DESCRIPTION = "Badass description"
urlpatterns = [
path('docs/', include_docs_urls(title=API_TITLE, description=API_DESCRIPTION)
That creates my docs, which is fantastic. But it is accessible by everyone even though I have set my permissions and authentications for all the endpoints to private. I did this like this in my configs:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAdminUser',
)
}
But even with this set I can access all site of the docs.
So I am looking for a way to protect my URLs which I imported and thus have no classes or methods at my disposal to protect. (So I can't use any decorators on them). Then I tried using the login required decorator on my URL like so:
path('docs/', login_required(include_docs_urls(title=API_TITLE, description=API_DESCRIPTION))),
but it throws me the following error: django.template.exceptions.TemplateDoesNotExist: registration/login.html
Is there a way of protecting such URLs?
Help is very much appreciated! Thanks a lot in advance!
EDIT: I figured I could pass permission classes as argument to my URL, and indeed Pycharm marks it as an option. So I put:
path('docs/', include_docs_urls(title=API_TITLE, description=API_DESCRIPTION, permission_classes= "rest_framework.permissions.IsAdminUser")),
That throws me the error str object not callable. Any ideas on how I could pass my permission classes there maybe?
For anyone ever having this problem. It is possible by doing the following:
from rest_framework.permissions import IsAdminUser
path('api/docs', include_docs_urls(
title=API_TITLE, description=API_DESCRIPTION, permission_classes=[IsAdminUser]
))
I wish this inbuilt API documentation tool was documented better. Took me a while to even find out that this exists....

RESTful API: is it good to have foreign key which sends to different version of the API?

We are developing a RESTful API using Django REST framework, and we decided to handle foreign keys showing a URL to the relation's resource, for example:
GET https://url_to_api/api/v2/foo/1:
{
"id": 1,
"bar": "https://url_to_api/api/v2/bar/6/",
"baz": "https://url_to_api/api/v3/baz/4/"
}
This is a GET request for the foo's ID 1 on the version 2 of the API.
Django REST framework returns the URL to the v2 for the relation with the bar entity because /api/v2/bar/6/ sends to a view which is only used by this version, but it returns an URL to v3 (https://url_to_api/api/v3/baz/4/) for the relation with the baz entity, because its view is the same as the v2's one, and Django REST framework reverse engine returns the first result for the URL which sends to the same view.
I have a couple of questions about this behaviour:
is it a normal behaviour or we are missing or doing something wrong?
is it good to have URLs which sends to a different version of the API?
Any other ideas on how to manage this?
This is a normal behaviour in DRF. So either you can specify like this or else you specify custom for everything.
# bookings/urls.py
urlpatterns = [
url(r'^$', bookings_list, name='bookings-list'),
url(r'^(?P<pk>[0-9]+)/$', bookings_detail, name='bookings-detail')
]
# urls.py
urlpatterns = [
url(r'^v1/bookings/', include('bookings.urls', namespace='v1')),
url(r'^v2/bookings/', include('bookings.urls', namespace='v2'))
]
For further reference, refer this

Django - Cannot remove parameter from URL after redirect

The authentication process of the instagram api returns an access token attached as a parameter to the redirect url, e.g. redirect.com/#access_token=xxx
I am currently working on getting the access token, saving it in a session and then redirecting to a different url. However after the redirect i simply get 'http://example.com/your-name/search/#access_token=xxx' and the parameter is still there. I have tried using
GET = request.GET.copy()
GET.pop('#access_token')
but I get a 'KeyError' although the the key 'access_token' is clearly there.
See below my urls.py
from django.conf.urls import url
From . import views
urlpatterns = [
url(r'^search/', views.get_name, name='get_name'),
url(r'^', views.index, name='index'),
]
and the below view.py
def index(request):
token = request.session.get('token')
if not token:
token=request.GET.get('#access_token')
request.session['token'] = token
return HttpResponseRedirect(redirect_to='http://example.com/your-name/search/')
if you have url with "#something" at the end (called "fragment"), e.g. "http://myserver/#something" and you will click on such a link, the fragment part will not be sent to the server, so you have no way to read it.
This behavior is described in RFC3986 section 3.5:
Fragment identifiers have a special role in information retrieval
systems as the primary form of client-side indirect referencing,
allowing an author to specifically identify aspects of an existing
resource that are only indirectly provided by the resource owner. As
such, the fragment identifier is not used in the scheme-specific
processing of a URI; instead, the fragment identifier is separated
from the rest of the URI prior to a dereference, and thus the
identifying information within the fragment itself is dereferenced
solely by the user agent, regardless of the URI scheme.
as such, fragment cant be used in server processing or redirects.

ExtJS 5 application + Django rest framework CORS error when changing URL of store

I am developing a ExtJS application that uses a Django-rest-framework service. I am using CORS headers to allow fetching the data from the service (https://github.com/OttoYiu/django-cors-headers).
What happens is that at a point in time I want to change the URL from the store. And when I do that I get the following error:
XMLHttpRequest cannot load http://10.98.0.241:8000/reacsearch/as?_dc=1418831884352&page=1&start=0&limit=25. The request was redirected to 'http://10.98.0.241:8000/reacsearch/as/?_dc=1418831884352&page=1&start=0&limit=25', which is disallowed for cross-origin requests that require preflight.
In the settings.oy I define the following properties for the CORS
CORS_ALLOW_METHODS = (
'GET',
'OPTIONS'
)
CORS_ORIGIN_ALLOW_ALL = True
This works fine when I use URLs to list all the elements in my database, however when I change the store for another URL I get the error above. Also the link works fine in the browser.
The store url change is made this way:
var store = Ext.getStore(storeName);
store.getProxy().setUrl(newURL);
store.load();
The difference between the views, is that the two that work on the application are viewsets, while the other is just a generic list
class Example1viewset(viewsets.ModelViewSet):
"""
API endpoing that allows metabolites to be viewed.
"""
queryset = examples1.objects.all()
serializer_class = Example1Serializer
class Example1SearchList(generics.ListAPIView):
serializer_class = Example1Serializer
def get_queryset(self):
queryset = Example.objects.all()
if 'attr' in self.kwargs:
queryset = queryset.filter(Q(attribute1__contains=self.kwargs['attr']) | Q(attribute2__contains=self.kwargs['abbr']))
return queryset
Like I mentioned both examples work fine in the browser (even accessing through other computers in the network), however in the application when changing the URL of the store I get the CORS error. Does anyone has any idea why this is happening?
Thank you.
Edit:
Just for clarification, the problem is not in changing the url of the store. As I tried to set those urls as defaults, but they are not working when accessing from the application.
My urls.py file:
router = routers.DefaultRouter()
router.register(r'example', views.Example1ViewSet)
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^reacsearch/(?P<attr>.+)/$', Example1SearchList.as_view()),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
Can it be that the problem is related with the fact that I am not adding the search list to the router?
Edit2
Problem solved since I was trying to fetch data from a different domain. I changed the type of store to jsonp in Extjs, and I also allowed my rest service to render data as jsonp.
Just a reminder if anyone comes accross this same problem, it is necessary to add ?format=jsonp to the store url:
http://my/url/?format=jsonp
Since it looks like an alternate solution was found, I'll explain what the issue appeared to be as well as why the alternative works.
XMLHttpRequest cannot load first url. The request was redirected to 'second url', which is disallowed for cross-origin requests that require preflight.
The issue here is that you are telling Django to enforce the trailing slash, which makes it automatically redirect urls without a trailing slash to urls with a trailing slash, assuming that one exists. This is why, as stated in the error, the request was redirected to the second url, which you can tell has the missing trailing slash. This is controlled by the APPEND_SLASH Django setting which is True by default.
The problem is that when CORS is doing a preflight request, which is what allows it to determine if the request can be made, there must be a valid response at the requested URL. Because you are redirecting the request, the preflight request fails and you're stuck without your information.
You can fix this by adding the trailing slash in your code. There appear to be a few solutions for doing this with ext, but I personally can't recommend a specific one. You can also manually set the url to use the trailing slash, which sounds like what you were doing previously.
Or you can use JSONP...
You've found the alternative solution, which is to use JSONP to make the request instead of relying on CORS. This gets around the preflight issue and works in all major browsers, but there are some drawbacks to consider. You can find more information on CORS vs JSONP by looking around.
You're going to need CORS if you want to push any changes to your API, as JSONP only supports GET requests. There are other advantages, such as the ability to abort requests, that also comes with CORS.