How to exclude drf router path from drf_spectacular SERVERS setting - django

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.

Related

Wagtail, CORS and Django-Cors-Headers. How to enable CORS so that AXIOS can get to the endpoints

I'm trying to enable the Wagtail API so I can do some exciting stuff in Vue. However, I've followed all the instructions for enabling the API in Wagtail and there is no mention of CORS. Well, it's less than a paragraph.
I use Django-Cors-Headers on all my projects and set it up as I normally would, but for some reason, the API still won't allow access.
Does anyone have any suggestions on how to allow a CORS connection to the Wagtail API endpoints using DJANGO-CORS-HEADERS?
This might be helpful. I can't talk for wagtail since I'm not using it in this particular project, but with the help of that and looking at the syntax for django 2's middleware, I came up with this:
class CorsMiddleware(object):
def __init__(self,get_response):
self.get_response=get_response
def __call__(self, request):
print('test')
response=self.get_response(request)
response["Access-Control-Allow-Origin"] = "*"
return response
Also make sure that when you're inserting the middleware into your settings.py, that you include the filename as well as the module name and component name, i.e. 'AppName.FileName.CorsMiddleware' instead of what's suggested on the linked site to put 'AppName.CorsMiddleware'. Maybe typo maybe old syntax ¯\_(ツ)_/¯
(The * might be a slight security risk, but i only needed it in development so i wasn't rly putting too much thought into it)

Django API with different levels

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.

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.

Integrating Sphinx and Django in order to require users to log in to see the documentation

I am curious if it is possible to hide sphinx documentation inside a django app so that only people who log in can see it. It seems to me that since sphinx creates its own structure and that Django uses the urlconf to define which pages a user can see, that it wouldn't be possible to combine the two. Although there must be some combining since the Django website likely uses django and sphinx. I am wondering if anyone has any insight or if they can point me in the right direction.
Thank You in Advance!
Sphinx builds your docs into HTML files, so in most cases this docs should be served by your web server rather then Django. However Django is able to serve static files as well.
You can use the django.views.static.serve function to do this and wrap this function with login_required. E.g:
from django.views.static import serve
from django.contrib.auth.decorators import login_required
urlpatterns += patterns('',
url(r'^docs/(?P<path>.*)', login_required(serve), {'document_root': '/path/to/sphinx/build/html'}, 'docs'),
)
However this configuration will be considered a bad practice in production environment as in this case Django will serve both html and css/js files from your sphinx theme.
The first improvement you can do here is to serve /path/to/sphinx/build/html/_static/ with apache/nginx or whatever you use.
The more proper way is to serve docs with apache/nginx and make it handle the auth itself. Unfortunately I made a quick Google search but did not find a way to use Django's User model to handle http_auth in apache or other. Alternatively you can use something like mod_sendfile or X-Accel modules - http://www.wellfireinteractive.com/blog/nginx-django-x-accel-redirects/ In a nutshell - Django app checks permission if user can view the file and add special header to response containing file path. Webserver will serve this file instead of original message from django

How to alter django settings based on current request?

I'm running multiple sites from a single django instance, and expect the framework to eventually serve several hundred sites from one or several installations.
I need to patch some django settings based on the current request. I've written some middleware to monkey patch the settings, but I need these settings to be patched before the middleware gets invoked because other apps aren't taking the monkey-patched changes (i.e. apps get run then middleware gets run so the apps don't use the monkey-patched settings).
I should also add this is mainly for the benefit of third-party apps that I haven't written, so I don't want to go round adding decorators or anything like that because that would mess up my upgrade path.
So:
How can I get access to the current request in an app's init.py file?
Will an app's init.py get called once per request or only once? If it's only once, how else could I do this so I can manipulate the settings once per request?
Is it safe to do this kind of monkey patching? I know it makes code a bit more opaque, but I don't want to use different wsgi files per site because I want to allow users to edit some of these settings and have my monkey patching come from the database.
Is there a better solution that would allow certain settings to be stored in the database?
This module - django-tupperware does what you are asking about: https://bitbucket.org/jiaaro/django-tupperware/
Give it a try.
Never ever ever change settings on the fly. You cannot predict how the application may one day be deployed, and in most of them the project will fail in.. interesting ways.
If you really want to have hundreds of sites which will be dynamically reconfigured all the time, you might try to Djangos django/conf/__init__.py which is used to supply the settings to the rest of the framework. You might be able to alter it in a way it depends on the request and it's Host field. Or you'll get many interesting failures at the very least.
My solution to this problem is to give each request, ALL the settings and update them on the fly with a middleware, how do i do this?
Is rather simple:
This is the middleware that does it all
from django.conf import settings
class DumpObject: pass
class Settings(object):
def process_request(self,request):
request.settings = DumpObject()
for setting in dir(settings):
if not setting.startswith('__'):
setattr(request.settings, setting, getattr(settings,setting))
The DumpObject is just there so i can use the object.property notation, it could have been a dictionary, but i wanted to keep some similarity in the syntax.
This assumes that none of your settings name starts with __ which is a fair assumption.
So if i want to override a particular setting, i don't keep a settings file i do it in this middleware. like this:
class Settings(object):
def process_request(self,request):
request.settings = DumpObject()
for setting in dir(settings):
if not setting.startswith('__'):
setattr(request.settings, setting, getattr(settings,setting))
if 'mydomain' in str(request.host): #this is thanks to django-hosts project
request.settings.GOOGLE_ANALYTICS_ID = '89298393-238'
Of course this doesnt take into account the problem of accessing the settings the old way
from django.conf import settings
settings.GOOGLE_ANALYTICS_ID = 'the value in settings.py'
But it really doesn't matter because you will probably only want to change the settings on the context of having a request object, available.