2011
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

  • Vijay K Ramesh

    Don’t reinvent the wheel, at least without first looking at the original blueprints.

    http://code.google.com/p/minidetector/ does what you’re trying to do, but better.

  • http://www.mattotodd.com Sullerton

    Vijay,
    Thanks for posting a comment, and glad you have an opinion, but I have to argue that particular code is less efficient and actually does not take the solution all the way like mine does. Let me explain why:

    1. minidetector loops through an array of strings (each string a mobile browser name) and it checks to see if the string is in the HTTP_USER_AGENT. This is performed on every incoming request. YIKES!! In my take on it, I compile a regex of the browser versions once on app load and reuse the compiled regex for each request. Using regex is much faster, and performance is important for something that is going to be run on every request to your site. Also, minibrowser only looks for browser names, and not versions numbers, which my regex does as well

    2. minibrowser adds a mobile flag to each request like mine does, but what then? This is actually the reason i wrote the post, I wanted a way to use different templates when that flag is set, without having to throw an IF statement in every view. Using a template loader along with the middleware is the full solution and so that’s why I felt the need to write the post.

  • http://butenas.com Ignas

    Worth to try :) Thanks for sharing.

  • http://alexdutton.co.uk/ Alexander Dutton

    As an aside, if you’re after capabilities-based detection then using the wurfl database might be the way to go. A project I worked on had some middleware that would add a device attribute to the request¹. Matching is a bit slow, but cached indefinitely afterwards.

    ¹ see https://github.com/mollyproject/mollyproject/blob/master/molly/wurfl/middleware.py

  • http://closedbracket.com Flaviu Simihaian

    Good stuff. Thanks.

  • NN

    Hi !
    Where should I put the mobile_templates folder?
    Let me know.
    Because I always get “Template does not exit” message.

    Thanks
    NN

  • http://www.mattotodd.com Sullerton

    You should put it at the same level (sibling) as your existing “templates” folder.

  • Oluwaseun

    Cool stuff. Its however difficult to decide where each snippet of code would be? If you could give a source code link to download it , that would be really great. Nice work dude. Saved me loads of time> And when I saw the minidetector and how much string it had to parse at every call, I thought to myself this would have some optimization hit. SO I guess what you did with precompiling the regex is a great one. Nice work.

  • http://www.fattybeagle.com/2011/07/17/django-mobile-site-template-loading/ Django mobile site template loading | follow your nose

    [...] http://sullerton.com/2011/03/django-mobile-browser-detection-middleware/ outlines a nice, clean way of accomplishing this in Django with middleware and a custom template loader: [...]

  • Grant Eagon

    Thanks for the middleware! I had used the same site’s nginx regex, but this is much simpler for deployment.

    I did do one thing differently. Instead of writing my own loader, I just wrote a function in my app’s views.py http://dpaste.com/581582/

    This had a couple advantages for me:
    1. I could change the mobile dir name with just a setting.
    2. The method of path construction accounts for subdirectories – useful if your templates directory has subdirectories.
    3. If there is not a mobile template for the request, the app simply renders to the non-mobile template.

    End result looks something like this:
    return render_to_response(getRequestTemplate(‘main/foo.html’), context)

    Thanks again!

  • http://www.mattotodd.com Sullerton

    Thanks for the input Grant. I know mine is def unpolished.

  • Arshavski Alexander

    Thank you a lot. I searched for a solution for 2 days and it’s just what I needed. Thanks again.

  • Ben D

    That’s really cool. Works so easily and quickly. I’m still new to django but had it up and running in no time.

  • yeago

    why not just set a cookie? that way the user could easily break out of the mobile version.

  • http://sullerton.com/2011/09/django-mobile-browser-detection-middleware-part-2/ Sullerton » Django mobile browser detection middleware – (part 2)

    [...] wrote my last post right after I found the solution and promised myself I’d revisit it with another post once I [...]

  • Rachelturner

    @ Vijay K Ramesh Yes I agree I found the google version worked better for me also.

    luminessairreviews

  • http://www.dailybathroom.com Albert

    I agree with you

  • Rebuttal

    Yeah right. The minidetector is both slower and the features aren’t as nice. Unless you like slower code (looping through a list instead of a basic regular expression match), there is no way you’d prefer it over this solution. Unless of course you are Vijay posting under a fake name, trying to get people to pay attention to your less-capable project…

  • Idel

    Good post. Look also my open source project called apache mobile filter. The main features are device detection, image rendering and mobile switcher. The mag supports several device repository such as wurfl, detectright, 51degrees.
    For more info: http://apachemobilefilter.org

  • Penugondla Veerabrahmaiah

    very good post, i certainly love this website, keep on it

  • http://www.allprivacyscreen.com/ claire

    Gan, punya tutorial membuat database mysql dengan phpmyadmin ga? Saya cari-cari bahan di internet belum nemu yang pas. Saya mau buat tabel relasi sederhana untuk full teks, cuma data teksnya banyak sampai ribuan, jadi ga bisa masukkan entri datanya satu per satu. Rencananya saya mau pakai csv. tapi masih newbie nih bingung cara bikin querynya.

  • http://twitter.com/matinfo Mathieu Meylan

    Thanks! Looks good!

  • Anuj Acharya

    Awesome!!