Django, paths and URLs

Michael Trier has a blog post on how he keeps his Django projects portable from one directory to another. This is something that gave us a hard time at work too and that we managed to fix (differently.)

To give you a brief idea of what we do, we usually develop small web sites for small companies. Because of the pains of using PHP, we moved all our new development to Django and have been satisfied. There are some areas that have caused us trouble however, and the number one pain is deployment. Some things are out of our hands, such as restarting Apache when a Python file is modified. But having to change a bunch of directories and URLs whenever we deployed, that was way too annoying not to be fixed.

We have a basic Django project skeleton that we now use whenever we start a new site. It is called WSF (short for “WebSite Framework”.) Before we had WSF, we started ever new project with the startproject command, and did everything anew over and over. The really sucky part was that between my designer and me, code would sometimes break because we did not keep our working copy in directories with the same names. And with the demos and the production servers having different directory names, it was evident that we needed to do something about that.

Our web sites usually go through four different file system paths:

  • My working copy
  • The working copy of my designer
  • Our demo server
  • The production server

We also needed our projects to work with different base URLs:

  • Development URLs (directly at the root)
  • Demo URLs (under /demos/client/wsf/)
  • Production URLs (under /wsf/ for technical reasons)

Paths

The first problem we concentrated on were the file system paths. Because the settings file in Django is a Python script, it was possible to use the path manipulation libraries to achieve portability. There are two paths you want to set in the settings.py module: MEDIA_ROOT and TEMPLATE_DIRS.

By using os.path, it was possible to set the correct paths dynamically:


PROJECT_PATH = os.path.dirname(os.path.abspath(__file__))

MEDIA_ROOT = os.path.join(PROJECT_PATH, 'media')

TEMPLATE_DIRS = (
    os.path.join(PROJECT_PATH, 'templates'),
)

The magic __file__ variable is the name of current module, in this case, settings.py. We use abspath to get the complete path, and we truncate the file name with dirname. This fixed all our path related problems, it was now easy to move the application between different folders.

URLs

Our next problem were the URLs. When we developed on our own machine with the built-in development server, the URLs were simply http://server:9090/foo/. When we moved the project to the demo server (served by Apache), the base URL was http://demoserver/demos/client-domain.com/wsf/. The reason for the trailing /wsf/ is that all of our projects have the name wsf, so for the import statement to work in Python, the folder needs to be named wsf too. We use .htaccess files to tell Apache to server the directory as a Django application.

We fixed the changing URL problem by defining a variable in the settings file called BASE_URL. In development mode, this would be set to the empty string, and on the demo server, we’d change it to ‘demos/client-domain.com/wsf/’.

Our urls.py file used this setting:


from django.conf.urls.defaults import *
from django.conf import settings

urlpatterns = patterns(
    '',
    (r'^%s' % settings.BASE_URL, include('wsf.site.urls')),
)

And in the site/urls.py file, the URLs were defined normally. By using the {% url %} tag in the templates and the reverse() function in the Python code, our URLs were portable…

But not all of them. There was no way to use {% url %} with the media files. We fixed this problem with a filter:


from django.conf import settings

@register.filter
def media(file):
    '''
    Concatenate MEDIA_URL and file.
    '''

    return settings.BASE_URL + file

So whenever we need to display a media file, we do the following:


<img src="{{ "images/foo.jpg"|media }}" />

and the correct URL will be used. We also use this filter in our CSS files (they are not served statically) for when we use the url() command.

Conclusion

With these simple settings, when we move an application from development to the demo or the production server, we need only change the BASE_URL setting and the rest of the project will work as expected.

I would say that this whole path and URL thing needs to have a better engineered solution before 1.0 is released. With PHP, it’s extremely easy to move an application from one server to another, and restarting the server is not necessary whenever a .php file is modified. The Django hackers really need to get together and figure a way to make this easier, because it’s really hard to sell the rest of the framework if putting an application online is so painful.

8 Responses to “Django, paths and URLs”

  1. Ville Säävuori Says:

    For me, ease of deployment is definitely one of the greatest things about Django. Unified settings and testing environments should be basic stuff for every web-team, no matter how big or small.

    That said, our company works almost always on professional non-shared server environments where stuff like root acces to the production server is taken granted. We use Capistrano to automate deployment as much as possible and we also try to make our development environment as deployment-friendly as it can be.

    I remember those PHP-projects where one would hack things together working live on the production server. Oh what horror that was. Development servers are for development and production servers need to be touched only when deploying — and even then preferably only in a fully automated way.

    Regarding to your template filter for media urls, there is a default context processor in Django that adds MEDIA_URL variable to each RequestContext instance. So your image url would become something like src=”{{ MEDIA_URL }}images/foo.jpg”

    Documentation: http://www.djangoproject.com/documentation/templates_python/#django-core-context-processors-media

  2. gnuvince Says:

    Ville: We decided to go with the media filter because we sometimes pass a media path to a tag, so this works:

    {% foo “images/foo.jpg”|media %}

    Thanks for the input!

  3. Smokinn Says:

    I’d be interested in reading a post about what these pains that forced you to another language were.

    Also, Ville, you talk like that doesn’t ever happen in Django. I don’t know since I’ve never tried it but I know for a fact that you can still go ahead and edit files on the production server in Rails so I suspect this is more of a personal discipline than language issue.

    I currently work in PHP and our deployment is dead simple. It’s one command: svn co. Everyone works on their own dev drive and commits working code that gets checked out on the test server. Once we’re satisfied that everything’s in working order we deploy to the various production servers with an svn checkout. I don’t see why it should be more complicated than that.

  4. gnuvince Says:

    Smokinn: I guess the usual pains most people have with PHP sans framework. No automatic admin interface to add information to a database, doing the user account thing all over again every time, writing form validation for nth time, doing multi-lingualization, putting display and control code in the same file, etc. And I’m not even talking about the pains of the language itself: no namespaces, inconsistent function names, inconsistent argument order, no higher order functions, the sucky error hangling support, etc.

    One could ease those pains with a few custom libraries or by using an already-existing framework. We tried Symfony for an internal application, it was a fucking pain to use, so we dumped it promptly.

    We had success with Django in our trial application and decided to keep going at it. As far as development goes, it’s a much smoother experience than PHP.

    And the reason why PHP is simple to deploy is because of .php extension. All those sites have these http://foo.com/bar.php addresses, so it’s easy for Apache to know when to call PHP. You *could* have a similar setup with Python; Spyce does it.

  5. sinelaborenihil Says:

    Interesting post, definitely some tips I will be using. What does your wsf look like ? is it just a dir structure/files you copy and use as a basis for new projects?

  6. gnuvince Says:

    sinelaborenihil: Yeah, just an already started project with an already started application with a few already defined URLs, a few media files we always use, a few scripts to make deployment easier, etc.

  7. Mike Says:

    If you add the following to your Apache .conf file, you don’t need to restart httpd when you change the Python code:

    MaxRequestsPerChild 1

  8. Mike Says:

    Also, the link you give is now:

    http://michaeltrier.com/2007/12/21/using-unipath-to-keep-thing-portable

Leave a Reply