Distributing Mediafiles with Django Apps

Some reuseable Django applications need mediafiles (CSS stylesheets or JavaScript files) to function properly. One common approach is to document, that the user has to copy these files to the MEDIA_ROOT directory.

Another example is django.contrib.admin. The recommended way here is to configure your webserver to serve the media folder inside contrib/admin under a special URL and configure a settings variable named ADMIN_MEDIA_PREFIX to this URL. All paths to stylesheets and scripts are adjusted according to this setting in the app.

I prefer not to use an Apache Alias-Directive to serve the admin media, but just to set up a symlink from the media folder to my document root.

Most of my deployments end up with a document root layout like this:

$DOC_ROOT/.htaccess
$DOC_ROOT/app.wsgi
$DOC_ROOT/admin_media/
$DOC_ROOT/site_media/

$DOC_ROOT is some path to the Apache VirtualHost document root, the .htaccess and app.wsgi files tell mod_wsgi how to serve the Django project via wsgi. The site_media directory is the one (or a symlink to) configured via settings.MEDIA_ROOT and the admin_media directory is a symlink to django/contrib/admin/media. The setting ADMIN_MEDIA_PREFIX is set to /admin_media/.

Up to this point everything works fine. Now let's see how to add mediafiles from other django apps to the mix.

Adding Mediafiles from other Apps

One possible way to handle application-specific mediafiles would be to do it like contrib.admin but I think this would lead to a lot of settings and a lot of folders/symlinks in the document-root and therefore a bunch of manual work on every deployment. The more apps with mediafiles are used the more work has to be done.

While rebuilding my Website with Django I ended up with a few apps which should distribute their own JavaScript and CSS files to be as reuseable as possible. To make the deployment easy and semi-automatic I came up with a pattern I will introduce now. I'm sure someone has done this before, but nevertheless this is how I've done it.

Every reuseable Django app with mediafiles gets a directory app_media (don't worry, this will be configurable on a per-app basis) and inside the app_media directory will be a directory with the name of the app itself. This is the same best practise as with templates, which should also reside inside the app in a directory named templates/<app_name>/. So from now on app-specific mediafiles reside in app_media/<app_name>/.

Now comes a single convention I introduced with this approach: The app can assume that its mediafiles are accessible at {{ MEDIA_URL }}<app_name>/*.

This means a stylesheet for example will be added to templates of the app like this:

<link rel="stylesheet" href="{{ MEDIA_URL }}app_name/css/style.css" type="text/css" />

To make this possible every app_media/<app_name>/ directory will be symlinked to MEDIA_ROOT/<app_name>/. A symlink has the advantes that no matter where the source of the app is stored, if it's updated the mediafiles are also updated automatically.

Make it work

Now you might ask, how this is better than any other approach and you are right, I've only introduced a new convention. To make this really easier than current manual approaches I've written some code, which automatically does the symlinking for you whenever you run the syncdb command by using Django's signals framework.

Here is the code:

import os
import sys
from django.conf import settings
from django.db.models import signals
from django.core.management.color import color_style

def link_app_media(sender, verbosity, **kwargs):
    """
    This function is called whenever django's `post_syncdb` signal is fired.
    It looks if the sending app has its own media directory by first checking
    if ``sender`` has a variable named ``MEDIA_DIRNAME`` specified and falling
    back to ``settings.APP_MEDIA_DIRNAME`` and as a last solution just using
    'app_media'. If a directory with this name exists under the app directory
    and has a subdirectory named as the app itself, this subdirectory is then
    symlinked to the ``MEDIA_ROOT`` directory, if it doesn't already exist.

    Example:

        An app called ``foo.bar`` (as listed in INSTALLED_APPS) needs to
        distribute some JavaScript files. The files are stored in
        `foo/bar/media/bar/js/*`. in `foo/bar/models.py` the follwoing is defined:

            MEDIA_DIRNAME = 'media'

        Now, whenever `manage.py syncdb` is run, the directory
        `foo/bar/media/bar` is linked to MEDIA_ROOT/bar and therefore the
        JavaScript files are accessible in the templates or as form media via:

            {{ MEDIA_ROOT }}bar/js/example.js

    Note: The MEDIA_DIRNAME is specified in the models.py instead of the
    __init__.py because the imported models.py module is what gets passed as
    ``sender`` to the signal handler and because apps need a models.py anyway
    to get picked up by the syncdb command.

    The symlink will not be created if a resource with the destination name
    already exists.

    """
    app_name = sender.__name__.split('.')[-2]
    app_dir = os.path.dirname(sender.__file__)

    try:
        APP_MEDIA_DIRNAME = sender.MEDIA_DIRNAME
    except AttributeError:
        APP_MEDIA_DIRNAME = getattr(settings, 'APP_MEDIA_DIRNAME', 'app_media')

    app_media = os.path.join(app_dir, APP_MEDIA_DIRNAME, app_name)
    if os.path.exists(app_media):
        dest = os.path.join(settings.MEDIA_ROOT, app_name)
        if not os.path.exists(dest):
            try:
                os.symlink(app_media, dest) # will not work on windows.
                if verbosity > 1:
                    print "symlinked app_media dir for app: %s" % app_name
            except:
                # windows users should get a note, that they should copy the
                # media files to the destination.
                error_msg   = "Failed to link media for '%s'\n" % app_name
                instruction = ("Please copy the media files to the MEDIA_ROOT",
                    "manually\n")
                sys.stderr.write(color_style().ERROR(str(error_msg)))
                sys.stderr.write(" ".join(instruction))

signals.post_syncdb.connect(link_app_media)

The code should be put in <app>/management.py (prefered) or in <app>/managment/__init__.py (if the app also contains management commands) or in any other file that will be read whenever manage.py syncdb runs.

The symlink from app_media/<app_media>/ to MEDIA_ROOT/<app_media> will only be created if it doesn't already exist and if no other file or directory with the name exists, therefor it will not destroy any existing data.

Configuration options

I promised earlier that the name of the app_media directory is a configurable option. It is already described in the docstring of the listed code but in a nutshell this are your options (in order of resolution, the example assumes you want to use my_mediafiles as directory name):

Conclusion

First the one major drawback: this approach will not work on Windows systems, as there are some problems with symlinks I don't fully understand. Windows users will still need to copy the app mediafiles to their MEDIA_ROOT, but I think most production deployments of Django projects happen on *nix systems, where symlinks work fine.

Now some positive notes: it will cause no harm, if the code exists multiple times (in more than one app and/or in the project itself), as it will only create the symlink(s) once, if it/they don't exist.

For me this little code snippet has made the deployment process for apps with their own mediafiles much more streamlined and therefore I wanted to share it with the community. I'm sure this is not for everyone, but not matter if you like the solution or not please don't hesitate to tell me why. One point against this solutions is, that it is not very explicit. It will do stuff even if you haven't configured it to do so, therefore it might not be a good idea to put this into an resuable app published as open source, but if you work on a project, where team members agree to use this way it might save you some time.


Kommentare