In Django, How to Add Regional Prefix to Most Paths - django

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.

Related

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.

Best way to achive the opposite of PREPEND_WWW (e.g. redirect from "www" to root domain)?

Django's PREPEND_WWW plus the Common middleware automatically redirects all root domain traffic to the "www." domain. How do I best achieve the opposite? Under django? Apache? Ngnix?
Where, possible, you'll want to do this kind of redirection outside of django. Here are instructions for Nginx and Apache. (You'll need to adjust the config according to desired non-www or www perference.).
As for why to do it outside of django, the answer is performance. There isn't a need here to have django process the request and response, since it can be done earlier in the request lifetime. This means you can save your django processes for when you actually need them.
The only reason you should use PREPEND_WWW, or similar custom middleware, is when you don't have access to the server configuration (such as some shared hosting environments). It's used as a last resort.
As #DanielB it is better for performance to do this outside Django.
But if you want to do it in Django anyway (e.g. you want to learn, or you don't care about performance, or you want to test if your test server doesn't use Apache, or whatever), then you can use this code which I found here:
class RemoveWwwMiddleware():
def process_request( self, request ):
try:
if request.META['HTTP_HOST'].lower().find('www.') == 0:
from django.http import HttpResponsePermanentRedirect
return HttpResponsePermanentRedirect(request.build_absolute_uri().replace('//www.', '//'))
except:
pass

Can I force Django’s per-site cache to use only each page’s path as its key?

I’ve developed a Django site. There’s pretty much a 1-to-1 relationship between model instances in the dabatase, and pages on the site.
I’d like to cache each page on the site (using memcached as the cache back-end). The site isn’t too big — according to a back-of-an-envelope calculation, the whole thing should fit into a fairly small amount of RAM — and the data doesn’t change particularly frequently, so the entire site could effectively live in the cache.
However, when the data does change, I want the cache to reflect that immediately, so ideally I’d like each model instance to be able to clear its own page from the cache when saved.
The way I imagined I’d do that is to cache pages with their URL as the key. Then each model instance can use its URL (which it knows via get_absolue_url()) to clear its page from the cache.
Can I make Django’s per site caching mechanism use page URLs as the cache key?
I don't know of any option to control the cache key, and the implementation in Django doesn't suggest there is any. The code to generate the cache key for a request through the cache middleware lives in django.utils.cache.get_cache_key (to know where to fetch from the cache) and learn_cache_key (to know where to set the cache). You could monkey-patch these functions not to take headers into account like this:
from django.utils import cache
from django.conf import settings
def get_path_cache_key(request, key_prefix=None):
if key_prefix is None:
key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
return cache._generate_cache_key(request, [], key_prefix)
# passing an empty headerlist so only request.path is taken into account
cache.get_cache_key = get_path_cache_key
cache.learn_cache_key = get_path_cache_key
This will internally take an MD5 hash of the path, add a potential prefix, and also take the current locale (language) into account. You could further change it to omit the prefix and the language. I would not recommend using the plain path without hashing it, as memcached does not allow keys longer than 250 characters or containing whitespaces, according to the documentation. This should not be a problem because you can just apply get_path_cache_key to the URL from get_absolute_url() as well and clear that page.

Creating 2 django sites that share 90% data and code

I have two closely related sites, a main site and a mobile site, hosted as a django app. They'll have a lot of the same functionality and need to access the same data. The main difference is the templates will be different and the way the site is structured will be different.
I have two separate virtual hosts, one for each (though I don't have to do it that way). My first thought was that the Django sites framework helps to solve this, but the docs don't seem to describe my use case.
Can someone give me a hint to know if I'm on the right track? The urls.py will need to be different since, for example, the homepage is completely different between the apps. The main goal is that for the data in the two different apps to be shared and the code to manage that does not need to be duplicated.
From the main site:
User submits an item that is stored in the model
From the mobile site:
User views a list of items and see the one just entered on the main site
User gives a 5 star rating on the recently added item
From the main site:
User views a list of highly rated items and the recently added item (which now has a high rating) shows up on the list.
Have a look at this answer to a similar question. Basically you can just use the same views and just return different templates based on the user-agent.
Also, if you structure your application logic so that it is broken up into different "apps" in django terms, then you can re-use them if you need different flows with similar components.
Hopefully this gets you off and running.
UPDATE:
So lets say you have your main site http://www.mainsite.com/ which has it's own urls.py models.py and views.py that makes your functionality for the main site. Then you have http://www.m.mainsite.com/ which has it's own set of urls, and views. Then you can just import the main site's models and use them in the mobile sites views.
OK, both answers are great and contributed to what I chose for my final solution.
In the settings.py file there is an option called ROOT_URLCONF. I created two settings.py files, called settings_desktop.py and settings_mobile.py and in each of these used the following code:
from settings.py import *
ROOT_URLCONF = 'myapp.urls_mobile'
(or in the case of the desktop, myapp.urls_desktop)
This actually gives a lot of cool features such as being able to use different template directories for each site, though really I'm not going to do that.
Then I created two versions of the wsgi file where the only difference was this line:
os.environ['DJANGO_SETTINGS_MODULE'] = 'myapp.settings_mobile'
or
os.environ['DJANGO_SETTINGS_MODULE'] = 'myapp.settings_desktop'
In each of the virtual hosts the only difference is the WSGIScriptAlias line that points to the different wsgi file for each host.
This allowed me to effectively use one django app that could easily accommodate both sites.
Thanks for helping work out a good solution to this.
I did something very similar once. My way of solving this problem of multiple urls.py was something like this:
Create two urlconf, one for each site;
Create a new Middleware:
from django.utils.cache import patch_vary_headers
class DomainMiddleware:
def __init__(self):
pass
def process_request(self, request):
#avoid problems when reaching the server directly trough IP
host = request.META.get('HTTP_HOST', None)
if host is None: return
host = host.split(':')[0] #remove port number
if host is mobile:
urlconf = "mobile.urls"
else:
urlconf = "default.urls"
request.urlconf = urlconf
def process_response(self, request, response):
patch_vary_headers(response, ('Host',))
return response
Check also why you have to do the patch_vary_headers on the docs.

How to make Django url dispatcher use subdomain?

I have a vague idea on how to solve this, but really need a push :)
I have a Django app running with apache (mod_wsgi). Today urls look like this:
http://site.com/category/A/product/B/
What I would like to do is this:
http://A.site.com/product/B
This means that the url dispatcher some how needs to pick up the value found in the subdomain and understand the context of this instead of only looking at the path. I see two approaches:
Use .htaccess and rewrites so that a.site.com is a rewrite. Not sure if this does the trick since I don't fully understand what the django url dispatcher framework will see in that case?
Understanding how the url dispatcher DO work I could write a filter that looks at valid sub domains and provides this in a rewritten format to the url dispatcher code.
Any hints or solutions are very much appreciated! Thanks.
Have you looked at django.contrib.sites? I think a combination of that, setting SITE_ID in your settings.py, and having one WSGI file per "site" can take care of things.
EDIT: -v set.
django.contrib.sites is meant to let you run multiple sites from the same Django project and database. It adds a table (django.contrib.sites.models.Site) that has domain and name fields. From what I can tell, the name can mean whatever you want it to, but it's usually the English name for the site. The domain is what should show up in the host part of the URL.
SITE_ID is set in settings.py to the id of the site being served. In the initial settings.py file, it is set to 1 (with no comments). You can replace this with whatever code you need to set it to the right value.
The obvious thing to do would be to check an environment variable, and look up that in the name or domain field in the Site table, but I'm not sure that will work from within the settings.py file, since that file sets up the database connection parameters (circular dependency?). So you'll probably have to settle for something like:
SITE_ID = int(os.environ.get('SITE_ID', 1)
Then in your WSGI file, you do something like:
os.environ['SITE_ID'] = 2
and set that last number to the appropriate value. You'll need one WSGI file per site, or maybe there's a way to set SITE_ID from within the Apache setup. Which path to choose depends on the site setup in question.
The sites framework is most powerful where you use Site as the target of a ForeignKey or ManyToManyField so that you can link your model instances (i.e. records) to specific sites.
Mikes solution is correct if you want to have multiple sites with same apps with different content (sites module) on multiple domains or subdomains, but it has a drawback that you need to be running multiple instances of the Django process.
A better solution for the main problem about multiple domains or subdomains is to use a simple middleware that handles incoming requests with the process_request() function and changing the documented urlconf attribute (link) of the request object to the URLconf you want to use.
More details and an example of the per-request or per-domain URL dispatcher can be found at:
http://gw.tnode.com/0483-Django/
Try adding a wildcard subdomain: usually *.