Django API with different levels - django

Currently, the project I'm working on has an API like the following:
api/
visitors/ # Endpoint returning list of visitors
visitor-rates/ # Endpoint returning visitors per time
gift-shop-orders/ # Endpoint returning purchases at the gift shop
order-rates/ # Endpoint returning purchases per time
What I'd like to do is group the visitor and gift shop endpoints under their own sub-heading. For example:
api/
visits/
visitors/
rates/
gift-shop/
orders/
rates/
Note that visits and gift-shop only have the job of listing out the URLs that are available under the respective subheading. So, a GET request to /api/visits should return:
{
"visitors": "/api/visits/visitors",
"rates": "/api/visits/rates"
}
In other words, I'd like /api/visits/ and /api/gift-shop/ to have the same behavior as the default router view used by Django for displaying the endpoints available in the root of the API (/api/).
I've tried simply nesting just the URLs together. So, suppose I've defined routers for the visitor endpoints and shop endpoints. I've tried:
api_patterns = [
url(r'^visits/', include(visitor_router.urls)),
url(r'^gift-shop/', include(shop_router.urls)),
]
urlpatterns = [
url(r'^api/', include(api_patterns)),
]
This makes it so that requests to /api/visits/ and /api/gift-shop/ respond correctly. However, if I go to /api/, no links to /api/visits or /api/gift-shop are given.
This leads me to believe that I need to nest the routers themselves, not just the URLs. I haven't seen any documentation on doing this, though, and my attempts at coming up with a custom solution have only led to other issues. Does anyone know if there is a simple, standard way to do this that I am missing?
tl;dr: How do I nest Django routers?

The short answer is you don't. If you're using ViewSets as a way to simplify your views, you need to stick to the rules of ViewSets, and if you want to have nested routes, you can do it with the #detail_route and #list_route decorators, but they don't express your resources as the rest of the framework does, and it's a manual labor.
The alternative, is using a third-party package called drf-nested-routers, which is really well done and can definitely do what you're expecting.

Related

How to exclude drf router path from drf_spectacular SERVERS setting

I would like to verify some best practices when using drf_spectacular with a django project.
I have an API in a django project where all the endpoints use the api/ prefix, i.e. in urls.py, I'm using.
path('api/', include('myapp_api.urls')),
The myapp_api project'ss urls.py uses the default router, so something like
router = routers.DefaultRouter()
router.register(r'assets', views.AssetsViewSet)
By default, this means that the swagger docs will present all the endpoints as something like...
/api/assets/{}/
instead of
/assets{}/
At this point, the swagger UI's test calls will work just fine because it will correctly call https://example.com/api/assets{}/. It just looks a bit messy in SwaggerUI so it's not ideal. I'm getting the impression that the /api is superfluous and should be essentially "handled" by the "servers" value.
A problem arises when I set the OpenAPI server object, which for drf_spectacular is the SERVERS setting, e.g.
SPECTACULAR_SETTINGS = {
'SERVERS': [{'url': 'https://example.com/api'}],
}
This will result in failures with SwaggerUI test calls because they try to sent requests to the following (not the /api/api)
https://example.com/api/api/assets/{}/
A secondary problem is that the /api/ prefix will still appear on all the SwaggerUI listed endpoints.
I seem to have two options:
I could go to my urls.py and use path('', include('myapp_api.urls')) instead of path('api/', include('myapp_api.urls')) but that's not desirable because the prefixing seems to make sense in this context.
I could use a drf_spectacular preprocessing hook and tweak all the endpoints it's generating.
e.g.
SPECTACULAR_SETTINGS = {
'SERVERS': [{'url': 'https://example.com/api'}],
'PREPROCESSING_HOOKS': ['my_preprocessing_hooks.strip_the_api_prefix']
}
def strip_the_api_prefix(endpoints, **kwargs):
for i in range(0, len(endpoints)):
temp = list(endpoints[i])
if temp[0].startswith('/api'):
temp[0] = temp[0][4:]
endpoints[i] = tuple(temp)
return endpoints
My question is, this all seems like a brittle hack and I'm wondering if I'm missing something. I would like to know if I'm following best practices etc.
I'm pretty sure I need to be setting at least one OpenAPI server value, so today it's https://example.com/api which could always be the 'bleeding edge', but later there could be a https://api.example.com/v1 if there's multiple major versions in the future that I want to maintain. My understanding is that the OpenAPI server value is important for client applications so that there's less need to guess the non-path part of the URL when interacting with an API.
I'm presuming that it's best practices to not have prefixes for all the endpoints that are documented in the SwaggerUI, but it seems that drf_spectacular is pulling them automatically from the urls.py files, which is why the /api keeps getting added.
So in short, is there a better way to handle this situation that using a preprocessing hook?
I've not used drf-spectacular before but I had a quick look at the docs and may be able to give you some ideas.
For your DRF urls - path('api/', include('myapp_api.urls')),. For different API version you would have different url files, even different views files. For example, non versioned files being your latest "bleeding edge":
myapp_api/urls/urls.py # latest and greatest
myapp_api/urls/v1_urls.py # older supported urls
myapp_api/views/views.py # latest and greatest
myapp_api/urls/v1_views.py # older supported urls
Then you would import the different versions in your main url file:
path('api/', include('myapp_api.urls.urls')),
path('api/v1/', include('myapp_api.urls.v1_urls')),
For your drf-spectacular, You may not need to implement SERVERS but if you do, it may be possible to do the below. I haven't used this package before though. Here's the documentation I've looked at for this settings.
SPECTACULAR_SETTINGS = {
'SERVERS': [
{'url': 'https://example.com/api'},
{'url': 'https://example.com/api/v1'},
],
}
Having the URLs and views setup with versioning forces versioning as the default. Although when you create a new "bleeding edge" API, it will involve renaming files and imports. If you don't want this, an alternative setup could be:
You keep track of the API by versioning them in the file names, where v1 is the oldest, and you increment for each iteration. So if you had v1, v2 and v3, v3 would be the latest. This would be urls/v1_urls.py, urls/v2_urls.py and so on. You would then only update the main urls file to point to the latest version. For example:
# Older supported APIs
path('api/v1/', include('myapp_api.urls.v1_urls')),
path('api/v2/', include('myapp_api.urls.v2_urls')),
# Latest API
path('api/', include('myapp_api.urls.v3_urls')),
If v4 became the latest, you would move v4 into the latest and move v3 into the oldest and change the URL to be api/v3/.
This combined with versioned named files would make it easier to delete retired versions and also add new version without having to rename imports etc.

Django Rest Framework: URL endpoint to add 'Favorite/Like/Rate' functionality when using Viewsets?

I created a "Recipe" viewset and I would like the functionality of people to be able to favorite different recipes.
My Viewset is very simple and it looks something like this:
class RecipeViewset(ListModelMixin, RetrieveModelMixin, GenericViewSet):
queryset = Recipe.objects.filter(enabled=True)
serializer_class = RoutineSerializer
My urls.py is using a SimpleRouter (Right now I dont have any url patterns so im leaving it empty:
urlpatterns = []
router = routers.SimpleRouter()
router.register('recipes', views.RecipeViewset, basename='recipes')
urlpatterns += router.urls
This creates 2 endpoints for the list and detail:
www.example.com/recipes
www.example.com/recipes/1
I have several options in mind of how the endpoint to favorite the recipe could look like:
www.example.com/recipes/1/favorite
www.example.com/recipes/favorite/1
www.example.com/favorite/recipes/1
www.example.com/recipes-favorite/1
Which would be the best approach and does Django provide an easy way to achieve this?
Could Viewset #action decorator be used for this?
PS: The user will send the Auth Token in the headers so it's not required to be sent in the URL.
This seems like a question that will most likely be answered with opinions rather than facts, since API design is very subjective to the contract between development and consumers.
Here are some links for API sub resource design tips: [1] [2].
However to make use of nested resources in DRF I think that you should use drf-nested-routers, since they provide a similar interface to what you've already have.
For that I think it would be easier to make this endpoint www.example.com/recipes/1/favorite, since it already has examples on the documentation and seem to work just fine.

Differences between RESTful API and urlpatterns router in Django

Am new to web developpement and wondering the differences between:
Django Restful API
and
standard Django URL routers
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', views.index, name='Index'),
url(r'^getvalue/$', views.get_points, name='Get Points'),
url(r'^putvalue/$', views.put_points, name='Put Points'),
]
What are the benefits of setting Django restful API when interacting with Javascript components since both are JSON sending URL ?
Before understanding this you have know that,
REST API concept.
HTTP verbs(REQUEST METHOD)
REST API
REST API is nothing but very special. Just remember one thing this is a concept where we can use proper use of HTTP VERBS. Like, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS etc....
HTTP VERBS
I already told you the names of HTTP VERBS. Think what we do normally?? Basically I do, I use POST for updating db row, I use POST for DELETE a row. But in REST API concept we can't do like this kinds of nasty things. When we are going to delete something we need to use DELETE
Links
You may read this, https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
It was quite some time since the question had been asked, but I thought I'd provide an answer as it bothered me as well.
The main reason why you want to use Django's routers is convenience: once you declared them once, you don't have to go through the same cumbersomeness as persistent declaration of urls in urls_patterns.
However, the url_patterns can still be used if for any reason you want to have a specific hard-coded url.

In Django, How to Add Regional Prefix to Most Paths

I'm converting an e-commerce site to be region (e.g., US, EU) specific, so it will basically feel like a different site to visitors based on the content they'll see, even though it's actually one site (for many reasons). Most of the paths on my site will become region-specific, by prefixing with the region at the beginning of the path e.g., '/us/' (I could convert all however if it made it dramatically easier).
My plan:
Middleware identifies the region based on 1) request path, 2) session, or 3) guessing based on the IP address in that order, and it's set on the request object. Also, once they use a regional path, it gets stored as a session value. In this way the region context is carried across the URLs which are not region-specific.
The URL patterns which are region-specific have to be updated to match region, even though I've already detected the region in the middleware, since the logic was more complicated than just path. Nevertheless I must make it a parameter and pass into all of my views, for the next reason (reversing). Also, any path which is becoming regional, will have their previous patterns 301 redirecting to their regional paths.
In order to generate links which are region-specific, I have to update many calls to reverse() and {% url %} by adding the region argument. I wish there was some layer here I could customize to dynamically reverse the URLs with knowledge of the request.
My primary question is the best way to handle reversing (the last bullet). It feels like a lot of unnecessary work. I am open to better ways to solve the problem overall.
Updates:
I ruled out subdomains because they are known to be bad for SEO, of transferring authority. Also, I think subdomains can imply totally different setups whereas for now I will manage this as a single webapp.
As #RemcoGerlich points out, basically I want to add the automagic behaviors that LocaleMiddleware/i18n_patterns adds both in urlconf and in reversing.
I came up with several ways this could be done (the fourth is a bonus using subdomains). All assume a middleware that detects region and sets it on the request.
Following #RemcoGerlich's tip, mimic how Django handles the internationalization of URLs. LocaleMiddleware detects the language and sets the active language on that request (with a thread local variable). Then, that active language is used to form the URLs with i18n_patterns(), which actually returns a LocaleRegexURLResolver (which subclasses the normal resolver) instead of urls. I believe something similar could be done to support other types of prefixes.
A more brute force approach is to again store region not only in the request but again in a thread local variable as Django does for the active language. Update URLs to have a named argument for the region prefix and add to view arguments. Implement a custom reverse to add the region parameter. If inclined to do evil, this could be monkeypatched to avoid touching every single reverse and url template reference.
Use middleware to set the request.urlconf based on the region, to override the ROOT_URLCONF. This provides a whole different set of URLs for this request only. Create one new URLconf per region, which add their prefix and then include the base URLconf. No need to capture the region part of the path or mess with view parameters. Reversing the URLs "just works".
If you wanted to use subdomains, which I didn't, there's a Django App called django-hosts as referenced in this question: Django: Overwrite ROOT_URLCONF with request.urlconf in middleware.
For my application, overriding request.urlconf with middleware was the simplest and most elegant solution. Here's a fragment from the middleware:
# ... detect region first based on path, then session, and and maybe later IP address...
# Then force the URLconf:
if request.region == Region.EU:
request.urlconf = "mysite.regional_urls.eu_urls"
else:
request.urlconf = "mysite.regional_urls.us_urls"
I created one new URLconf per region, but they are DRY one-liners:
urlpatterns = create_patterns_for_region(Region.EU)
These referenced a template that combined both the URLs I wanted to be regional with those I wanted to leave "bare":
from django.conf.urls import patterns, include, url
def create_patterns_for_region(region):
return patterns(
'',
# First match regional.
url(r'^{}/'.format(region.short), include('mysite.regional_urls.regional_base_urls')),
# Non-regional pages.
url(r'', include('mysite.regional_urls.nonregional_base_urls')),
# Any regional URL is missing.
url(r'^{}/.*'.format(Region.REGION_PREFIX), error_views.Custom404.as_error_view()),
# Attempt to map any non-regional URL to region for backward compatibility.
url(r'.*', RegionRedirect.as_view()),
)
And finally a redirect view for backward compatibility:
class RegionRedirect(RedirectView):
""" Map paths without region to regional versions for backward compatibility.
"""
permanent = True
query_string = True
def get_redirect_url(self, *args, **kwargs):
self.url = "/" + self.request.region.short + self.request.path
return super(RegionRedirect, self).get_redirect_url(*args, **kwargs)
Make sure to update caching to include region. ;)
No real answer, just two suggestions:
Can't you use subdomains? Same idea with the middleware but makes it independent of the URL generation.
Django supports your idea but for languages instead of regions (docs here), maybe you can adapt that, or at least look at how it solves your problem.

adding new url patterns to urls.py on the fly in django

I'm trying to add new url patterns to the projects urls.py(not an apps urls) on the fly. I couldn't find anything about this on stackoverflow!
Edit:
I'm writing a simple scaffolding app. For a given model, I create forms, views, templates, and urls.py for an app on the fly. The last thing is to add(attach) urls.py of the app to the urls.py of the project automatically.
Django routing does not allow such dynamics, as the routing table is built once in the application startup and never refreshed. Even if it where refreshable then you should communicate the routing table changes across different server processes using database, sockets, Redis pubsub or such mechanism and you would be bending Django framework for something it was not designed to do.
Instead, as the suggestion is, you need one generic regex hook to match the all URLs you want to be "dynamic". Then, inside the view code of this generic URL you can do your own routing based on the full input URL and the available data (E.g. from database). You can even build your own Django URL resolver inside the view if you wish to do so, though this might not be a problem-free approach.
Generally, the better approach to solve the situation like this is called traversal. Django does not natively support traversal, but other Python web frameworks like Pyramid support traversal.