django-mobile 0.7.0
Detect mobile browsers and serve different template flavours to them.
License: BSD
Keywords: django,mobile

=============
django-mobile
=============

.. image:: https://travis-ci.org/gregmuellegger/django-mobile.png
   :alt: Build Status
   :target: https://travis-ci.org/gregmuellegger/django-mobile

.. _introduction:

**django-mobile** provides a simple way to detect mobile browsers and gives
you tools at your hand to render some different templates to deliver a mobile
version of your site to the user.

The idea is to keep your views exactly the same but to transparently
interchange the templates used to render a response. This is done in two
steps:

1. A middleware determines the client's preference to view your site. E.g. if
   he wants to use the mobile flavour or the full desktop flavour.
2. The template loader takes then care of choosing the correct templates based
   on the flavour detected in the middleware.


Installation
============

.. _installation:

*Pre-Requirements:* ``django_mobile`` depends on django's session framework. So
before you try to use ``django_mobile`` make sure that the sessions framework
is enabled and working.

1. Install ``django_mobile`` with your favourite python tool, e.g. with
   ``easy_install django_mobile`` or ``pip install django_mobile``.
2. Add ``django_mobile`` to your ``INSTALLED_APPS`` setting in the
   ``settings.py``.
3. Add ``django_mobile.middleware.MobileDetectionMiddleware`` to your
   ``MIDDLEWARE_CLASSES`` setting.
4. Add ``django_mobile.middleware.SetFlavourMiddleware`` to your
   ``MIDDLEWARE_CLASSES`` setting. Make sure it's listed *after*
   ``MobileDetectionMiddleware`` and also after ``SessionMiddleware``.
5. Add ``django_mobile.loader.Loader`` as first item to your
   ``TEMPLATE_LOADERS`` list in ``settings.py``.
6. Add ``django_mobile.context_processors.flavour`` to your
   ``TEMPLATE_CONTEXT_PROCESSORS`` setting.

Now you should be able to use **django-mobile** in its glory. Read below of how
things work and which settings can be tweaked to modify **django-mobile**'s
behaviour.


Usage
=====

.. _flavours:

The concept of **django-mobile** is build around the ideas of different
*flavours* for your site. For example the *mobile* version is described as
one possible *flavour*, the desktop version as another.

This makes it possible to provide many possible designs instead of just
differentiating between a full desktop experience and one mobile version.  You
can make multiple mobile flavours available e.g. one for mobile safari on the
iPhone and Android as well as one for Opera and an extra one for the internet
tablets like the iPad.

*Note:* By default **django-mobile** only distinguishes between *full* and
*mobile* flavour.

After the correct flavour is somehow chosen by the middlewares, it's
assigned to the ``request.flavour`` attribute. You can use this in your views
to provide separate logic.

This flavour is then use to transparently choose custom templates for this
special flavour. The selected template will have the current flavour prefixed
to the template name you actually want to render. This means when
``render_to_response('index.html', ...)`` is called with the *mobile* flavour
being active will actually return a response rendered with the
``mobile/index.html`` template. However if this flavoured template is not
available it will gracefully fallback to the default ``index.html`` template.

In some cases its not the desired way to have a completely separate templates
for each flavour. You can also use the ``{{ flavour }}`` template variable to
only change small aspects of a single template. A short example:

.. code-block:: html+django

    <html>
    <head>
        <title>My site {% if flavour == "mobile" %}(mobile version){% endif %}</title>
    </head>
    <body>
        ...
    </body>
    </html>

This will add ``(mobile version)`` to the title of your site if viewed with
the mobile flavour enabled.

*Note:* The ``flavour`` template variable is only available if you have set up the
``django_mobile.context_processors.flavour`` context processor and used
django's ``RequestContext`` as context instance to render the template.

Changing the current flavour
----------------------------

The basic use case of **django-mobile** is obviously to serve a mobile version
of your site to users. The selection of the correct flavour is usually already
done in the middlewares when your own views are called. In some cases you want
to change the currently used flavour in your view or somewhere else. You can
do this by simply calling ``django_mobile.set_flavour(flavour[,
permanent=True])``. The first argument is self explaining. But keep in mind
that you only can pass in a flavour that you is also in your ``FLAVOURS``
setting. Otherwise ``set_flavour`` will raise a ``ValueError``. The optional
``permanent`` parameters defines if the change of the flavour is remember for
future requests of the same client.

Your users can set their desired flavour them self. They just need to specify
the ``flavour`` GET parameter on a request to your site. This will permanently
choose this flavour as their preference to view the site.

You can use this GET parameter to let the user select from your available
flavours:

.. code-block:: html+django

    <ul>
        <li><a href="?flavour=full">Get the full experience</a>
        <li><a href="?flavour=mobile">View our mobile version</a>
        <li><a href="?flavour=ipad">View our iPad version</a>
    </ul>

Notes on caching
----------------

.. _caching:

Django is shipping with some convenience methods to easily cache your views.
One of them is ``django.views.decorators.cache.cache_page``. The problem with
caching a whole page in conjunction with **django-mobile** is, that django's
caching system is not aware of flavours. This means that if the first request
to a page is served with a mobile flavour, the second request might also
get a page rendered with the mobile flavour from the cache -- even if the
second one was requested by a desktop browser.

**django-mobile** is shipping with it's own implementation of ``cache_page``
to resolve this issue. Please use ``django_mobile.cache.cache_page`` instead
of django's own ``cache_page`` decorator.

You can also use django's caching middlewares
``django.middleware.cache.UpdateCacheMiddleware`` and
``FetchFromCacheMiddleware`` like you already do. But to make them aware of
flavours, you need to add
``django_mobile.cache.middleware.FetchFromCacheFlavourMiddleware`` item before standard Django ``FetchFromCacheMiddleware``
in the ``MIDDLEWARE_CLASSES`` settings and ``django_mobile.cache.middleware.UpdateCacheFlavourMiddleware`` before 
``django_mobile.cache.middleware.UpdateCacheMiddleware`` correspondingly.

It is necessary to split the usage of ``CacheMiddleware`` because some additional work should be done on request and response *before* standard caching behavior and that is not possible while using two complete middlewares in either order

Reference
=========

``django_mobile.get_flavour([request,] [default])``
    Get the currently active flavour. If no flavour can be determined it will
    return *default*. This can happen if ``set_flavour`` was not called before
    in the current request-response cycle. *default* defaults to the first
    item in the ``FLAVOURS`` setting.

``django_mobile.set_flavour(flavour, [request,] [permanent])``
    Set the *flavour* to be used for *request*. This will raise ``ValueError``
    if *flavour* is not in the ``FLAVOURS`` setting. You can try to set the
    flavour permanently for *request* by passing ``permanent=True``. This may
    fail if you are out of a request-response cycle. *request* defaults to the
    currently active request.

``django_mobile.context_processors.flavour``
    Context processor that adds the current flavour as *flavour* to the
    context.

``django_mobile.context_processors.is_mobile``
    This context processor will add a *is_mobile* variable to the context
    which is ``True`` if the current flavour equals the
    ``DEFAULT_MOBILE_FLAVOUR`` setting.

``django_mobile.middleware.SetFlavourMiddleware``
    Takes care of loading the stored flavour from the user's session or
    cookies (depending on ``FLAVOURS_STORAGE_BACKEND``) if set. Also sets the
    current request to a thread-local variable. This is needed to provide
    ``get_flavour()`` functionality without having access to the request
    object.

``django_mobile.middleware.MobileDetectionMiddleware``
    Detects if a mobile browser tries to access the site and sets the flavour
    to ``DEFAULT_MOBILE_FLAVOUR`` settings value in case.

``django_mobile.cache.cache_page``
    Same as django's ``cache_page`` decorator, but wraps the view into
    additional decorators before and after that. Makes it possible to serve multiple
    flavours without getting into trouble with django's caching that doesn't
    know about flavours.

``django_mobile.cache.vary_on_flavour_fetch`` ``django_mobile.cache.vary_on_flavour_update``
    Decorators created from the ``FetchFromCacheFlavourMiddleware`` and ``UpdateCacheFlavourMiddleware`` middleware.

``django_mobile.cache.middleware.FetchFromCacheFlavourMiddleware``
    Adds ``X-Flavour`` header to ``request.META`` in ``process_request``

``django_mobile.cache.middleware.UpdateCacheFlavourMiddleware``
    Adds ``X-Flavour`` header to ``response['Vary']`` in ``process_response`` so that Django's ``CacheMiddleware`` know that it should take into account the content of this header when looking up the cached content on next request to this URL.


Customization
=============

.. _customization:

There are some points available that let you customize the behaviour of
**django-mobile**. Here are some possibilities listed:

``MobileDetectionMiddleware``
-----------------------------

The built-in middleware to detect if the user is using a mobile browser served
well in production but is far from perfect and also implemented in a very
simplistic way. You can safely remove this middleware from your settings and
add your own version instead. Just make sure that it calls
``django_mobile.set_flavour`` at some point to set the correct flavour for
you.

Settings
--------

.. _settings:

Here is a list of settings that are used by **django-mobile** and can be
changed in your own ``settings.py``:

``FLAVOURS``
    A list of available flavours for your site.
    
    **Default:** ``('full', 'mobile')``

``DEFAULT_MOBILE_FLAVOUR``
    The flavour which is chosen if the built-in ``MobileDetectionMiddleware``
    detects a mobile browser.
    
    **Default:** ``'mobile'``

``FLAVOURS_COOKIE_HTTPONLY``
    The value that get passed into ``HttpResponse.set_cookie``'s ``httponly``
    argument. Set this to ``True`` if you don't want the Javascript code to be
    able to read the flavour cookie.
    
    **Default:** ``False``

``FLAVOURS_COOKIE_KEY``
    The cookie name that is used for storing the selected flavour in the
    browser.  This is only used if ``FLAVOURS_STORAGE_BACKEND`` is set to
    ``'cookie'``.
    
    **Default:** ``'flavour'``

``FLAVOURS_TEMPLATE_PREFIX``
    This string will be prefixed to the template names when searching for
    flavoured templates. This is useful if you have many flavours and want to
    store them in a common subdirectory. Example:
    
    .. code-block:: python
    
        from django.template.loader import render_to_string
        from django_mobile import set_flavour

        set_flavour('mobile')
        render_to_string('index.html') # will render 'mobile/index.html'

        # now add this to settings.py
        FLAVOURS_TEMPLATE_PREFIX = 'flavours/'

        # and try again

        set_flavour('mobile')
        render_to_string('index.html') # will render 'flavours/mobile/index.html'
    
    **Default:** ``''`` (empty string)

``FLAVOURS_TEMPLATE_LOADERS``
    **django-mobile**'s template loader can load templates prefixed with the
    current flavour. Specify with this setting which loaders are used to load
    flavoured templates.
    
    **Default:** same as ``TEMPLATE_LOADERS`` setting but without
    ``'django_mobile.loader.Loader'``.

``FLAVOURS_GET_PARAMETER``
    Users can change the flavour they want to look at with a HTTP GET
    parameter.  This determines the name of this parameter.  Set it to
    ``None`` to disable.
    
    **Default:** ``'flavour'``

``FLAVOURS_SESSION_KEY``
    The user's preference set with the GET parameter is stored in the user's
    session. This setting determines which session key is used to hold this
    information.
    
    **Default:** ``'flavour'``

``FLAVOURS_STORAGE_BACKEND``
    Determines how the selected flavour is stored persistently. Available
    values: ``'session'`` and ``'cookie'``.
    
    **Default:** ``'cookie'``

Cache Settings
--------------

Django ships with the `cached template loader`_
``django.template.loaders.cached.Loader`` that doesn't require to fetch the
template from disk every time you want to render it. However it isn't aware of
django-mobile's flavours. For this purpose you can use
``'django_mobile.loader.CachedLoader'`` as a drop-in replacement that does
exactly the same django's version but takes the different flavours into
account. To use it, put the following bit into your ``settings.py`` file:

.. code-block:: python

    TEMPLATE_LOADERS = (
        ('django_mobile.loader.CachedLoader', (
              'django_mobile.loader.Loader',
              'django.template.loaders.filesystem.Loader',
              'django.template.loaders.app_directories.Loader',
        )),
    )

.. _cached template loader:
   https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.loaders.cached.Loader


Changelog
=========

0.7.0
-----

* `#64`_: Fixing ``cache_page`` decorator and splitting the
  ``CacheFlavourMiddleware`` into two middlewares. This follows the same
  strategy as Django did since quite a while. Please see `#64`_ for more
  details about why this is necessary.

  If you are using ``CacheFlavourMiddleware``, you need to replace it now with
  ``FetchFromCacheFlavourMiddleware`` and ``UpdateCacheMiddleware``.  Please
  consolidate the README for more information.

  Thanks to Yury Paykov for the patch.

.. _#64: https://github.com/gregmuellegger/django-mobile/pull/64

0.6.0
-----

* `#63`_: Django 1.9 support. Thanks to Alexandre Vicenzi for the patch.

.. _#63: https://github.com/gregmuellegger/django-mobile/pull/63

0.5.1
-----

* `#58`_: Fix Python 3 install issues related to unicode strings. Thanks to
  Zowie for inspiring the patch.

.. _#58: https://github.com/gregmuellegger/django-mobile/pull/58

0.5.0
-----

* Support for Django 1.7 and Django 1.8. Thanks to Jose Ignacio Galarza and to
  Anton Shurashov for the patches.

0.4.0
-----

* Python 3.3 compatibility, thanks Mirko Rossini for the patch.
* Dropping Django 1.3 and 1.4 support.

0.3.0
-----

* Dropping support for python 2.5 (it might still work but we won't test
  against it anymore).
* Fixing threading problems because of wrong usage of ``threading.local``.
  Thanks to Mike Shultz for the patch.
* Adding a cached template loader. Thanks to Saverio for the patch.

0.2.4
-----

* FIX: Cookie backend actually never really worked. Thanks to demidov91 for
  the report. 

0.2.3
-----

* FIX: set *flavour* in all cases, not only if a mobile browser is detected.
  Thanks to John P. Kiffmeyer for the report.

0.2.2
-----

* FIX: Opera Mobile on Android was categorized as mobile browser. Thanks to
  dgerzo for the report.
* Sniffing for iPad so that it doesn't get recognized as small mobile device.
  Thanks to Ryan Showalter for the patch.

0.2.1
-----

* Fixed packing issues that didn't include the django_mobile.cache package.
  Thanks to *Scott Turnbull* for the report.

0.2.0
-----

* Restructured project layout to remove settings.py and manage.py from
  top-level directory. This resolves module-name conflicts when installing
  with pip's -e option. Thanks to *bendavis78* for the report.

* Added a ``cache_page`` decorator that emulates django's ``cache_page`` but
  takes flavours into account. The caching system would otherwise cache the
  flavour that is currently active when a cache miss occurs. Thanks to
  *itmustbejj* for the report.

* Added a ``CacheFlavourMiddleware`` that makes django's caching middlewares
  aware of flavours. We use interally the ``Vary`` response header and the
  ``X-Flavour`` request header.

0.1.4
-----

* Fixed issue in template loader that only implemented
  ``load_template_source`` but no ``load_template``. Thanks to tylanpince,
  rwilcox and Frédéric Roland for the report.

0.1.3
-----

* Fixed issue with ``runserver`` command that didn't handled all request
  independed from each other. Thanks to bclermont and Frédéric Roland for the
  report.

0.1.2
-----

* Fixed unreferenced variable error in ``SetFlavourMiddleware``.

0.1.1
-----

* Fixed ``is_usable`` attribute for ``django_mobile.loader.Loader``. Thanks Michela Ledwidge for the report.

0.1.0
-----

* Initial release.