Howto combine DjangoRestFramework routers for different apps - django

I use Django 2.1 and DRF and a planning a quite larger application with many plugged-in apps. I'd like to have ONE /api URL for the DRF as endpoint, but allow each app to have a special model exposed over the REST endpoint, e.g.:
In the main urls.py:
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = [
# ...
path('api/', include(router.urls)),
# ...
]
and in foo_app/urls.py:
router = routers.DefaultRouter()
router.register(r'foomodel', FooModelViewSet)
Now, /api/foomodel produces a 404 Error. The foo_model/urls.py gets imported (a print statement there is printed at Django start), and all the other foo_model.urlpattern[path...] are recognized and work fine.
How can I define custom api model endpoints for a central api REST endpoint? I didn't find anything in the documentation.
Thanks in advance.

I simply achieved this by extending the router registries.
root urls.py
from ad.urls import router as ad_router
main_router = routers.DefaultRouter()
main_router.registry.extend(ad_router.registry)
ad.urls.py
from .api.urls import router as api_router
router = routers.DefaultRouter()
router.registry.extend(api_router.registry)
ad.api.urls.py
router = routers.DefaultRouter()
router.register(r'ad', AdViewSet)

In your main urls.py you would do something like this.
(This is for Django 1.8)
urlpatterns += patterns('',
url(r'^api/', include(patterns('',
url(r'^foo_app/', include('foo_app.urls')),
url(r'^bar_app/', include('bar_app.urls')),
url(r'^test_app/', include('test_app.urls')),
))))
this allows you to access all your endpoints this way
localhost:8000/api/foo_app/<foo_app_endpoint>
localhost:8000/api/bar_app/<bar_app_endpoint>
localhost:8000/api/bar_app/<test_app_endpoint>

Related

Django DRF Swagger: In urls.py SimpleRouter / DefaultRouter are not auto discovered by swagger docs

Django DRF Swagger docs are not showing the ModelViewSets API endpoints registered as ROUTERS (not urlpattern).
In the example below standard docs (rest_framework.documentation) are showing/documenting this "follow_up" API and swagger docs are not, total skip nothing is showing.
For urlpatterns all is good, and below code for 'this_is_showing' is being nicely documented:
from urls.py file
from rest_framework.documentation import include_docs_urls
from rest_framework.routers import SimpleRouter, DefaultRouter
from rest_framework_swagger.views import get_swagger_view
from . import views
schema_view = get_swagger_view(title=MY APP API')
router = DefaultRouter()
router.register("follow_up", views.FollowUpViewSet)
urlpatterns = [
url(r'^this_is_showing/$', views.SomeView.as_view(), name='view'),
url(r'docs/', include_docs_urls(
title='API Docs', public=True)),
url(r'^swag/', schema_view),
]
What am I missing?
django-rest-swagger==2.2.0,
djangorestframework==3.11.0
EDIT 1
django-rest-swagger Package not maintained anymore!
Moved to drf_yasg: great tool with swagger and reDocs inside.
The DRF docs suggest that if you want to get the auto-generated API list view you need to use the DefaultRouter. I wonder if the SimpleRouter lacks the introspective mechanism (or other hooks) that django-rest-swagger uses to get its information.
https://www.django-rest-framework.org/api-guide/routers/#defaultrouter
EDIT 1
The DRF-swagger docs say that their example uses the DRF example: https://django-rest-swagger.readthedocs.io/en/latest/
The DRF example uses the default router: https://github.com/encode/rest-framework-tutorial/blob/master/snippets/urls.py
EDIT 2
I believe you'll also need to include the router somewhere in your URL patterns. If you look here: https://github.com/encode/rest-framework-tutorial/blob/master/snippets/urls.py
Not only is the DefaultRouter being used, but the router that's registered is included in the URL patters:
from django.conf.urls import include, url
from rest_framework.routers import DefaultRouter
from snippets import views
# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'users', views.UserViewSet)
# The API URLs are now determined automatically by the router.
# Additionally, we include the login URLs for the browsable API.
urlpatterns = [
url(r'^', include(router.urls))
]

Django Rest Framework: incorrect hyperlink on second viewset of same model

I'm trying to provide two distinct APIs using DRF but I'm unable to get the second app to stop creating
hyperlinked references based on the first. It's essentially the same problem as Django Rest Framework with multiple Viewsets and Routers for the same object but I'm unable to get it working.
app1/urls.py:
router = SimpleRouter(trailing_slash=False)
router.register(prefix=r'article', viewset=app1.ArticleViewSet, basename=r'article')
urlpatterns = [path(f'', include(router.urls)]
app2/urls.py:
router = SimpleRouter(trailing_slash=False)
router.register(prefix=r'published', viewset=app2.ArticleViewSet, basename=r'published')
urlpatterns = [path(f'', include(router.urls)]
site/urls.py:
urlpatterns = [
path('app1/', include('app1.urls')),
path('app2/', include('app2.urls')),
]
While both viewsets are of the same model, the queryset & serializer for each is different.
When I GET an item from /app2/published, it has an app1 URL:
"url": "http://localhost:8000/app1/article/5461"
What I'm wanting is for items retrieved via app2 to have:
"url": "http://localhost:8000/app2/published/5461"
From looking at the docs, it appears that providing basename should do what I want, but I'm not having any luck with getting it to work.
Try the following code in your site/urls.py:
from app1.urls import router as app1_router
from app2.urls import router as app2_router
router = routers.DefaultRouter()
router.registry.extend(app1_router.registry)
router.registry.extend(app2_router.registry)
urlpatterns = [
path('', include(router.urls)), # default page to show api
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
You can see an example here, which has same structure as you need.

Hide Django rest framework Routers Api View Page

I am using DjangoRestFramework 3.3.2 for Routing in my django application. I have 6 different folders for 6 apps and 1 main project app. I have include all 6 apps urls into main url file. Following is my main url file.
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^vpc/', include('vpc.urls')),
url(r'^dss/', include('dss.urls')),
url(r'^rds/', include('rds.urls')),
url(r'^compute/', include('compute.urls')),
url(r'^iam/', include('iam.urls')),
]
And this is my one of app url file.
from django.conf.urls import url
from rest_framework import routers
import views.instance_views as instance
import views.snapshot_views as snapshot
router = routers.SimpleRouter()
router.register(r'instance', instance.IntanceViewSet, base_name='instance')
router.register(r'snapshot', snapshot.SnapshotViewSet, base_name='snapshot')
urlpatterns = []
urlpatterns += router.urls
Now my problem is when I open urls in browser, I can see whole url hierarchy. Which is not required.
How do I hide these rendered views. I don't want to show any extra information
I was able to hide view using:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
}
But I am still getting all urls under 1 app.
{"instance":"http://127.0.0.1:8000/compute/instance/","keypair":"http://127.0.0.1:8000/compute/keypair/","volume":"http://127.0.0.1:8000/compute/volume/","image":"http://127.0.0.1:8000/compute/image/","snapshot":"http://127.0.0.1:8000/compute/snapshot/"}
In your urls.py change the default router to simple router.
router = routers.SimpleRouter()
You should also add the following snippet in your production settings file to only enable JSONRenderer for the API, This will completely disable the web browsable API.
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
}
Just to update on the answers given. You do need to specify the SimpleRouter() router, but often the DefaultRouter() router is useful for viewing and debugging whilst in development.
With that in mind, I would advise doing the simple following step:
if settings.DEBUG:
router = DefaultRouter()
else:
router = SimpleRouter()
Then as normal:
from myproject.users.api.viewsets import UserViewSet
router.register(r'users', UserViewSet)
In settings.py DEBUG=False and your REST API wont show.

DRF Browsable API only shows one Router

Essentially, depending on the order in which I add my routes to my urlpatterns the browsable API will only show one router at a time. Here's my code:
urls.py:
from django.conf.urls import url, include
from rest_framework import routers
from .views import PlantViewSet
# url router
router = routers.DefaultRouter()
router.register(r'plants', PlantViewSet, base_name='Plants')
djoser_urls = [url(r'^', include('djoser.urls')), ]
urlpatterns = [
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^docs/', include('rest_framework_swagger.urls')),
# url(r'^', include(router.urls)),
# url(r'^', include('djoser.urls')),
] + djoser_urls + router.urls
This only displays the djoser urls:
However simply reversing the order in which I add the urls:
urls.py:
from django.conf.urls import url, include
from rest_framework import routers
from .views import PlantViewSet
# url router
router = routers.DefaultRouter()
router.register(r'plants', PlantViewSet, base_name='Plants')
djoser_urls = [url(r'^', include('djoser.urls')), ]
urlpatterns = [
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^docs/', include('rest_framework_swagger.urls')),
# url(r'^', include(router.urls)),
# url(r'^', include('djoser.urls')),
] + router.urls + djoser_urls
This only displays the router urls!
The same thing happens when I just use the include() lines I've commented out, whichever comes first in the list is the only router that gets displayed. Furthermore, no matter which router gets picked up the api-auth/ and docs/ urls are never shown. Is there anyway to get a unified api root without having to create my own custom view?
This doesn't have anything to do with Django REST framework, it happens because of how Django deals with duplicate urls.
You are trying to have a single url be handled by two different views: The DRF router index and the djoser root view. Django will only display the first view matching the search pattern that it finds, which is generally the first urls that are included in the url patterns.
Django REST framework will also not detect multiple routers that are available and group them together on the same page, which is sounds like you are hoping to see. Even if it could, djoser doesn't use a router so there is no way that DRF could actually know to include it.
Is there anyway to get a unified api root without having to create my own custom view?
So to answer the main question: No it is not possible for Django REST framework to automatically group these views together. You are going to need to create your own customer view to handle this.

Registering API in apps

With django-rest-framework I'm using the DefaultRouter
I want to provide APIs to several apps, so my question is can I do this in a django manner and put my router registrations in each app URLconf and have them appear either as one aggregate api or ideally in a namespaced way.
In other words if app1 contains modelA and modelB, while app2 contains modelC:
can I declare 2 routers that appear at mysite/app1/api and mysite/app2/api, or
can I have a single api at mysite/api which lists all three models yet register the individual models in their own app's urls.py
Something like
router = DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(include('app1.apis')
router.register(include('app2.apis')
Alternatively is there a simple way in which my router variable can be made available in each app's URLconf so that they can call router.register? I'm not sure if
urlpatterns = patterns('',
url(r'^snippets/', include('snippets.urls', namespace="snippets"))
...
url(r'^api/', include(router.urls)),
actually cause the code in app1/urls.py to be executed at that point so that it could call router.register somehow, so that the final url call includes all the app registrations as well as the project one.
UPDATE
Using a variation on Nicolas Cortot's option 2 I get my specific resource API to work, but it is not listed as an available resource in the root API at myserver\api\
I assume that somehow DefaultRouter creates it's own page definition and router.register adds entries to it. My current setup (and I think Nicholas's option 1 as well) create two separate routers, and only one can get displayed as the server root, with the setup below, myserver\api\ lists users but not snippets.
Here's my current setup:
project urls.py:
router = DefaultRouter()
router.register(r'users', views.UserViewSet)
urlpatterns = patterns('',
url(r'^admin/', include(admin.site.urls)),
url(r'^api/', include(router.urls)),
url(r'^api/', include('snippets.apiurls')),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
)
project/snippets/apiurls.py:
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
urlpatterns = patterns('',
url(r'^', include(router.urls)),
)
If I reverse the order of the entries in the project urls.py as:
url(r'^api/', include('snippets.apiurls')),
url(r'^api/', include(router.urls)),
then I get snippets listed but not users
I guess Django is serving the first matching route.
Unless someone can tell me otherwise I seem to need a single router variable to be passed around and added to somehow.
To get all apps in the same API root, you need to register all your apps with the same DefaultRouter.
One way to achieve this is to make a custom router, which intercepts the register call and propagates it to a shared router. You then use this shared router to get the api urls.
class SharedAPIRootRouter(SimpleRouter):
shared_router = DefaultRouter()
def register(self, *args, **kwargs):
self.shared_router.register(*args, **kwargs)
super().register(*args, **kwargs)
# if not py3: super(SharedAPIRootRouter, self).register(*args,**kwargs)
Then in each app:
# in app1/urls.py
router = SharedAPIRootRouter()
router.register(r'app1', App1ModelViewSet)
# in app2/urls.py
router = SharedAPIRootRouter()
router.register(r'app2', App2ModelViewSet)
In your main urls.py, you must ensure you import the app urls so that registration occurs before we ask for shared_router.urls
import app1.urls
import app2.urls
def api_urls():
return SharedAPIRootRouter.shared_router.urls
urlpatterns = patterns(
'',
url(r'^api/', include(api_urls())),
)
if you do not want to import the urls explicitly, you can do it by convention:
def api_urls():
from importlib import import_module
for app in settings.INSTALLED_APPS:
try:
import_module(app + '.urls')
except (ImportError, AttributeError):
pass
return SharedAPIRootRouter.shared_router.urls
This is possible by passing around a single router instance as follows.
Create a file called router.py or similar in your main project folder:
from rest_framework import routers
common_router = routers.DefaultRouter()
In each app's urls.py put:
from main.router import common_router as router
router.register(r'myapp-model-name', MyAppViewSet)
In your main urls.py put:
import my_app1.urls # to register urls with router
import my_app2.urls # to register urls with router
...
# finally import router that includes all routes
from main.router import common_router
urlpatterns = [
...
url(r'^api/', include(common_router.urls)),
...
]
Both options are possible. You can either expose the router or the urls in each app, and merge those into your global urls. I usually prefer using urls (option 2) because it gives more flexibility in each app: you can define extra non-api URLs as needed.
Option 1
In your global urls.py:
from app1.api.routers import router1
from app2.api.routers import router2
urlpatterns = patterns('',
url(r'^snippets/', include('snippets.urls', namespace="snippets"))
...
url(r'^app1/api/', include(router1.urls)),
url(r'^app2/api/', include(router2.urls)),
)
You can as easily use the same endpoint for both routers (as long as you're careful not to use conflicting routes):
urlpatterns = patterns('',
url(r'^snippets/', include('snippets.urls', namespace="snippets"))
...
url(r'^api/', include(router1.urls)),
url(r'^api/', include(router2.urls)),
)
Option 2
In appN/api/urls.py:
router = DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(include('app1.apis')
urlpatterns = patterns('',
url(r'^', include(router.urls)),
url(r'^misc/', some_other_view),
)
In your global urls.py:
urlpatterns = patterns('',
url(r'^snippets/', include('snippets.urls', namespace="snippets"))
...
url(r'^api/', include('app1.api.urls')),
url(r'^api/', include('app2.api.urls')),
)
Note that the urls modules do not need to be the same as the urls for standard views.
As a more advanced variant on #Grischa, I like to extend his approach:
In the main's routers.py:
from rest_framework import routers
api_v1_router = routers.SimpleRouter()
In the main's urls.py:
from django.urls import include, path
import app1.urls
from .routers import api_v1_router
# Register app urls
app1.urls.register(api_v1_router)
app2.urls.register(api_v1_router)
...
urlpatterns = [
...
path('v1/', include((api_v1_router.urls, 'v1'))),
...
]
In each app's urls.py:
from main.routers import api_v1_router
from .apis import MyAppViewSet1, MyAppViewSet2
def register(router):
router.register(r'myapp-model-name1', MyAppViewSet1)
router.register(r'myapp-model-name2', MyAppViewSet2)
Two advantages of this approach:
You can control the registration of the apps in the main urls.py
The flexibility of register(router) allows you to register to different routers, for example when using both v1 and v2 for versioning.