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):
- put
MEDIA_DIRNAME = 'my_mediafiles'
in themodels.py
of the app. This will only configure the name of the media directory in this app. - add
APP_MEDIA_DIRNAME = 'my_mediafiles'
to your projectssettings.py
. This will configure the name of the media directory for all apps (not very usefull if you are not in control of all apps). - do nothing and use the default name
app_media
for all apps.
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.
Hey, Arne! This is a very useful script, I’ll try this on my next project.
Geschrieben von Stefan 2 Tage, 14 Stunden nach Veröffentlichung des Blog-Eintrags am 16. März 2009, 10:26. Antworten
Genau darüber hab ich nachgedacht, wie ich es in PyLucid lösen möchte. Bin im Grunde auf eine ähnliche Idee gekommen. Allerdings nicht, das man signals.post_syncdb dafür nutzten könnte. Sehr gute Idee...
Werde ich in PyLucid v0.9 mal versuchen umzusetzen.
Geschrieben von jens 3 Monate nach Veröffentlichung des Blog-Eintrags am 18. Juni 2009, 08:39. Antworten
great article! This saved me from using 2 servers, one for media and one for django. Thanks a lot!
Geschrieben von noel 8 Monate nach Veröffentlichung des Blog-Eintrags am 11. Nov. 2009, 18:17. Antworten