03.20
EDIT: The code in this post has been refactored and packaged here, but this post explains how I solved my problem.
What is the proper way to handle browser traffic redirection and/or experience for mobile users?
I wanted to try out a simple version of LMD using jquery mobile. The current website is written using Django. All the business logic would remain the same, I just want to use different templates when the request comes from a mobile browser.
The solution I chose:
- write some middleware that would identify all incoming mobile requests
- use a custom template loader that returns a mobile template if its a mobile user
To write the middleware, I used the regex found at DetectMobileBrowser to search the user agent using the process_request method. It looks like this:
#custom.middleware.py import re try: from threading import local except ImportError: from django.utils._threading_local import local _thread_locals = local() reg_b = re.compile(r"android|avantgo|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|symbian|treo|up\\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino", re.I|re.M) reg_v = re.compile(r"1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\\-(n|u)|c55\\/|capi|ccwa|cdm\\-|cell|chtm|cldc|cmd\\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\\-s|devi|dica|dmob|do(c|p)o|ds(12|\\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\\-|_)|g1 u|g560|gene|gf\\-5|g\\-mo|go(\\.w|od)|gr(ad|un)|haie|hcit|hd\\-(m|p|t)|hei\\-|hi(pt|ta)|hp( i|ip)|hs\\-c|ht(c(\\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\\-(20|go|ma)|i230|iac( |\\-|\\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\\/)|klon|kpt |kwc\\-|kyo(c|k)|le(no|xi)|lg( g|\\/(k|l|u)|50|54|e\\-|e\\/|\\-[a-w])|libw|lynx|m1\\-w|m3ga|m50\\/|ma(te|ui|xo)|mc(01|21|ca)|m\\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\\-2|po(ck|rt|se)|prox|psio|pt\\-g|qa\\-a|qc(07|12|21|32|60|\\-[2-7]|i\\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\\-|oo|p\\-)|sdk\\/|se(c(\\-|0|1)|47|mc|nd|ri)|sgh\\-|shar|sie(\\-|m)|sk\\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\\-|v\\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\\-|tdg\\-|tel(i|m)|tim\\-|t\\-mo|to(pl|sh)|ts(70|m\\-|m3|m5)|tx\\-9|up(\\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\\-|2|g)|yas\\-|your|zeto|zte\\-", re.I|re.M) def get_current_request(): return getattr(_thread_locals, 'request', None) class RequestMiddleware(): def process_request(self, request): request.is_mobile = False if request.META.has_key('HTTP_USER_AGENT'): user_agent = request.META['HTTP_USER_AGENT'] b = reg_b.search(user_agent) v = reg_v.search(user_agent[0:4]) if b or v: #this is a mobile request request.is_mobile = True _thread_locals.request = request
Some people may want to actually redirect their users to a different url or subdomain if they find a mobile request, which you can easily do with this middleware if you’d like. Just do the redirection where it says #this is a mobile request.
You’ll notice I’ve also added my request object to a thread local varible (Please someone freak out and tell me why this is going to be my demise in the comments). If you are going to be redirecting users, you won’t need any of the thread local stuff. I am using it here because it allows me to easily get access to the request later on in my template loader as you’ll see below.
I chose setting a flag on the request object because LMD lives on AppEngine, so managing subdomains is not as easy as it would be using apache. But as far as I saw it, I have to run the regex on every request into my main domain to check for mobile anyways. Plus, I don’t really want a different app to handle my mobile requests, I just want to render a different template (writing your own template loader).
The one I wrote is based off django’s primary template loader, found on a post by Corey Oordt. It is super simple and only pulls from a single directory called mobile_templates. You can easily expand on this to have a tuple of directories in your settings file to loop through, much like TEMPLATE_DIRS works.
#custom.loaders.py from os.path import join from django.conf import settings from django.template import TemplateDoesNotExist from path.to.middleware import get_current_request def load_template_source(template_name, template_dirs=None): request = get_current_request() if hasattr(request, 'is_mobile') and request.is_mobile: try: #Note: I set a PROJECT_PATH in my settings like so: #PROJECT_PATH = os.path.realpath(os.path.dirname(__file__)) #This gives you easy access to the path to settings.py (usually the root of your project) filepath = join(settings.PROJECT_PATH, "mobile_templates", template_name) file = open(filepath) try: return (file.read(), filepath) finally: file.close() except IOError: pass raise TemplateDoesNotExist(template_name) load_template_source.is_usable = True
This loader will get the current request via the get_current_request() method I defined in my middleware (via the thread local varible). If it is a mobile request, I attempt to open the template from my mobile_templates folder. If a mobile template is not found it returns a TemplateDoesNotExist exception and django will continue on with other loaders until it finds a matching template. This is nice because it fails back to my default templates when I do not have a specific mobile version.
Another reason I set the request.is_mobile flag in the middleware is that it allows me to do specific mobile business logic in my app if I want to. I used it to add a mobile flag to my context dictionary so it can be used in any templates (in the case where I don’t need a totally different template for mobile, but just need to make a small change).
#custom.context_processors.py def mobile_browser(request): dict = {'mobile_browser': False} if hasattr(request, 'is_mobile'): dict['mobile_browser'] = request.is_mobile return dict
Make sure to add the middleware and template loader (and the context processor if you chose to use it) in your settings file:
#settings.py ... TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.debug', 'django.core.context_processors.i18n', 'custom.context_processors.mobile_browser' ) # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( 'custom.loaders.load_template_source', 'django.template.loaders.filesystem.load_template_source', 'django.template.loaders.app_directories.load_template_source', # 'django.template.loaders.eggs.load_template_source', ) MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'custom.middleware.RequestMiddleware', #'django.contrib.auth.middleware.AuthenticationMiddleware', )
Thanks to easel for pointing me in the direction of template loaders
Questions, feedback? I’d love to hear it.
…….continued in Part 2