Django – Local Settings Revisited

Settings in a Django project will differ between development and production instances. In my case, I develop my projects on my laptop running the built in Django webserver and the sqlite3 database, while the production systems use Apache and MySQL (on another server). Media paths, the DEBUG setting, and other attributes can differ.

After browsing around several web sources, I learned that the prevailing wisdom is to include an import of a local-settings.py file that in turn imports another settings file that contains the installation specific values. All of the settings files are included in version control, except local-settings.py, which is different for each installation. This setup has worked fine with my personal applications as well.

However, I learned this week that the way my central IT office allows access to servers that makes this method unworkable. I have been given easy access to the development server, where I have been able to create and modify the local-settings.py file as needed. What I was told this week is that I will have no access to the quality assurance or production servers, except through a deploy mechanism that copies the entire application directory from the dev server to QA, and from QA to production. This means that I can’t create a unique local-settings.py on each box.

Instead, I need to be able to determine the instance based on values available at run time. My server admin told me of an Apache environment variable called ‘tier’ that he maintains in the Apache configuration, but despite my efforts to find a way to access the variable, I was unsuccessful, If anyone out there can guide me in the right direction, please leave a comment.

One down side to using the Apache variable is that not all of my installations use that web server, so I’d still have to find an alternative (or use a default value) to properly identify other systems.

My next idea was to read the name of the server running the application, and using that to make the decision. The getfqdh() function of Python’s socket module returns the fully qualified domain name, which worked well to identify the instance. Here’s the code (your domain names may vary):

settings.py

## Determine the server environment.

import socket

fqdn = socket.getfqdn() ## Get the fully qualified domain name

SERVER_ENVIRONMENT = 'UNKNOWN'

if fqdn == 'dashdrum_laptop':          ## laptop
    SERVER_ENVIRONMENT = 'Laptop'
elif fqdn == 'dev.example.com':       ## dev server
    SERVER_ENVIRONMENT = 'DEV'
elif fqdn == 'qa.example.com':         ## qa server
    SERVER_ENVIRONMENT = 'QA'
elif fqdn == 'example.com':            ## production server
    SERVER_ENVIRONMENT = 'PROD'

A potential problem with this method is that the server name may change over time. More on this here.

Once I know the environment, I can import the proper settings file thusly:

settings.py

## Get server specific settings

try:
    if SERVER_ENVIRONMENT == 'Laptop':
        from laptop_settings import *
    elif SERVER_ENVIRONMENT == 'DEV':
        from dev_settings import *
    elif SERVER_ENVIRONMENT == 'QA':
        from qa_settings import *
    elif SERVER_ENVIRONMENT == 'PROD':
        from prod_settings import *
except ImportError:
    pass

Assuming that I have the correct domain names and the correct settings for each instance, I should be good to go in each environment.

Using CAS with Django – an Update

As I move closer to deploying my application in production, I have been rethinking my implementation of CAS in Django, as outlined in this earlier post,Using CAS with Django. Nothing major, but I don’t like the idea of commenting or uncommenting the code based on the server being used. Here’s what I came up with:

First, I added a new variable to settings.py called USE_CAS. This is set to False by default. Following that, I have some code that determines the server (out of scope for this post) and changes the variable to True if needed.

settings.py

USE_CAS = False

## Check for Production server - the real code is much better than this
if SERVER_ENVIRONMENT == 'Prod':
    USE_CAS = True

Later, I can use that variable to decide whether to execute the CAS specific portions of code:

settings.py

## django_cas settings

if USE_CAS:
    CAS_SERVER_URL = 'https://www.example.com/apps/account/cas/' 
    CAS_VERSION = '2'
    
    AUTHENTICATION_BACKENDS = (
        'django.contrib.auth.backends.ModelBackend',
        'django_cas.backends.CASBackend',
    )
    
    MIDDLEWARE_CLASSES += (
        'django_cas.middleware.CASMiddleware',
    )

## end django_cas settings
urls.py

# django_cas
if settings.USE_CAS:
    urlpatterns = patterns('',
    url(r'^accounts/login/$', 'django_cas.views.login',name='login'),
    url(r'^accounts/logout/$', 'django_cas.views.logout',name='logout'),
                           ) + urlpatterns
                           
# end django_cas

Now, to properly direct the logout link, I’m taking a different approach. My previous example showed how I changed the links on the top right of the page to point to the proper logout link by replacing that section of the admin template in admin/base_site.html. I probably could have passed in the USE_CAS variable in the context and then selectively modified the template, but I instead wanted to simplify the process, not make it more difficult. After searching around a little, I found an unrelated post about using redirects to handle legacy URLs. This worked great, as I was able to redirect both the logout and change password links to the proper destination. Note that the change password functionality is not part of my project, but is provided by the central IT organization that also provides the CAS services.

urls.py

# django_cas
if settings.USE_CAS:
    urlpatterns = patterns('',
    ('^admin/logout/$', 'django.views.generic.simple.redirect_to', 
            {'url': '../../accounts/logout'}),
    ('^admin/password_change/$', 'django.views.generic.simple.redirect_to', 
            {'url': 'https://www.example.com/apps/account/ChangePassword'}),
    url(r'^accounts/login/$', 'django_cas.views.login',name='login'),
    url(r'^accounts/logout/$', 'django_cas.views.logout',name='logout'),
                           ) + urlpatterns
                           
# end django_cas

Pretty slick, don’t you think? Now, I have an easy to maintain setup that will work in any of my development and production environments. Plus, I cut the number of code files affected from 3 to 2, and I have no more commenting to worry about.

I would love to hear how others have solved this or a similar issue. Please leave your comments below.