In Django, How to Add Regional Prefix to Most Paths

1.1k views Asked by At

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.
2

There are 2 answers

0
John Lehmann On BEST ANSWER

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.

  1. 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.

  2. 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.

  3. 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".

  4. 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. ;)

4
RemcoGerlich On

No real answer, just two suggestions:

  1. Can't you use subdomains? Same idea with the middleware but makes it independent of the URL generation.

  2. 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.