Note: This little how-to works great for me and the way I work. I use a Mac laptop to develop Django sites which have a life as a production site somewhere else. If you work another way (like in Windows, or with Apache for development instead of Django's dev server, or if you work straight off the production server), this might not help directly, but I hope it helps get you started on finding a way to solve this little annoyance on your own.
So I do a lot of Django development on my laptop. I have a bunch of Django sites which I built over the course of the past two years or so - most of which I have to go back and work on a little every now and then. Because they've been developed at different times, most go back to before the new QuerySet refactor merge, and a few go back to before the Autoescape revision. I'm also looking at building my next site with the newforms-admin branch.
I'm using subversion to check out the latest versions of Django. I usually check it out into /usr/local and then make a symbolic link to it in Python's site-packages folder. Because some of my sites raise errors when I run them with versions of Django released after they were written, I find myself changing the revision number of my laptop's Django occasionally (and by occasionally, I mean way too often).
To get around that, I figured out that I can do this little trick. Now, in my /usr/local I have all the different versions of Django I'm likely to need. Like so:
django/
django-newforms-admin/
django-pre-qsr/
django-pre-autoescape/
django-myclient/
The one called just django/ is the latest trunk revision... the one I usually use. The others are the revisions (or close approximations of the revisions) that I used when I made some of the older Django sites on my laptop. Each of these folders contains a folder called "django".
In the manage.py files for each of my sites where I can't or don't want to use the latest trunk revision, I just drop in a couple of lines right at the top:
import sys
sys.path.insert(0, "/usr/local/django-myclient/")
This drops the path to my preferred Django rev onto the start of the sys.path list, and Python uses the first django module it finds in it's path. Now whenever I use manage.py's shell, dbshell or runserver commands, I'm using the Django rev number I need without having to fool around with svn update each time I need to work on a different site.
This article describes the process we use at Capstrat to set up Django applications on WebFaction shared hosting. The instructions are specific to Mac OSX or Linux, but I've included a few additional notes for Windows users. It is assumed that you are comfortable working with the command line, that you have Python and Subversion client installed on your machine and that you have checked out and worked with Django before.
python <path-to-django>/bin/django-admin.py startproject <project-name><path-to-django> refers to the place where the Django source code exists on your machine.<project-name> will be the name of your new project.
cd /src/python
python /opt/local/lib/python2.5/django/bin/django-admin.py startproject mikesproject
cd /src/python/<project-name>
mkdir elements
mkdir elements/css # store all CSS here
mkdir elements/img # store all HTML design images here
mkdir elements/js # store JavaScript files here
mkdir elements/swf # store Flash files here
mkdir elements/media # store user-contributed media here
DEVELOPMENT = True
PROJECT_DIRECTORY = '/src/python/<project-name>/'
ELEMENTS = PROJECT_DIRECTORY + 'elements/'
# Catch elements URLs for working in development
if settings.DEVELOPMENT:
urlpatterns += patterns('',
(r'^elements/(?P<path>.*)$', 'django.views.static.serve', { 'document_root': settings.ELEMENTS, 'show_indexes': True }),
)
import settings
ADMIN_MEDIA_PREFIX in settings.py like so:
ADMIN_MEDIA_PREFIX = '/elements/admin/'
DATABASE_ENGINE = 'sqlite3'
DATABASE_NAME = PROJECT_DIRECTORY + 'db.sqlite3'
INSTALLED_APPS tuple in settings.py:
'django.contrib.admin',
python manage.py syncdb
cp settings.py wf-settings.py
DEVELOPMENT global to False. This will disable the static media server set up in step 3.2.PROJECT_DIRECTORY setting to the directory where your project will exist on WebFaction. Assuming that you set up your account as a Django account, it should look like:
PROJECT_DIRECTORY = '/home/<account-name>/webapps/django/<project-name>/
ssh <account-name>@<account-name>.webfactional.com
cd ~/webapps/subversion
less .htpasswd
htpasswd -D .htpasswd test
htpasswd -m .htpasswd <username>
(Enter the new password twice at the prompt.)
less .htpasswd
svn mkdir -m "initial project directory structure for <project-name>" http://svn.<account-name>.webfactional.com/<project-name> http://svn.<account-name>.webfactional.com/<project-name>/trunk http://svn.<account-name>.webfactional.com/<project-name>/tags http://svn.<account-name>.webfactional.com/<project-name>/branches
global-ignores = *.o *.lo *.la *.pyc *.tmproj settings.py .*
svn import . http://svn.<account-name>.webfactional.com/<project-name>/trunk/<project-name>
cd .. # go up one directory
mv <project-name> <project-name>_tmp # move the project to a temp directory
svn co http://svn.<account-name>.webfactional.com/<project-name>/trunk/<project-name> <project-name> # check out the project
cp <project-name>_tmp/settings.py <project> # copy the original setting.py file into the checked out project.
rm -rf <project_name>_tmp # remove the temporary project folder
cd ~/webapps/django
svn co http://svn.<account-name>.webfactional.com/<project-name>/trunk/<project-name> <project-name>
cd ~/webapps/django/<project-name>
cp wf-settings.py settings.py
/home/<account-name>/webapps/django/<project-name>/elements
cd ~/webapps/django/apache2/conf
SetEnv DJANGO_SETTINGS_MODULE myproject.settings
to read:
SetEnv DJANGO_SETTINGS_MODULE <project-name>.settings
cd ~/lib/python2.5
You may need to remove an existing copy of django: rm -rf django
svn co http://code.djangoproject.com/svn/django/trunk/django django
If you need a different revision than the latest trunk, do this: svn update -R <desired-revision> django
cd ~/webapps/django/lib/python2.5
rm -rf django # remove the WebFaction-provided copy of Django
ln -s ~/lib/python2.5/django django # create a symbolic link to your revision of Django
Note: Here, we have modified the project-specific Python lib folder that Apache uses when it runs Django. If you need to run different revisions of Django for two different applications on the server, you could alternatively check out the required revision in the project-specific lib folder, rather than using a symbolic link to the global copy of Django.
cd ~/webapps/django/<project-name>/elements
ln -s ~/lib/python2.5/django/contrib/admin/media admin
Note: the symbolic link name "admin" corresponds to the ADMIN_MEDIA_PREFIX in settings.py from step 3.3.
svn propset svn:ignore admin .
[miscellany] section, uncomment the "global-ignores" line and change it to look like this:
global-ignores = *.o *.lo *.la *.pyc *.tmproj settings.py .*
cd ~/webapps/django/<project-name>
svn ci -m "working copy is now set up on WebFaction"
cd ~/webapps/django/<project-name>
../apache2/bin/stop
../apache2/bin/start
alias python=/usr/local/bin/python2.5
source .bashrc to make the changes take effect. Now, running python will invoke Python 2.5, so Django will be available for import.
cd ~/webapps/django/<project-name>
svn update
../apache2/bin/stop
../apache2/bin/start
In the current development version of Django, model field validation in the admin has one difficult shortcoming: validator functions don't know if the fields they are validating are for an object that is being created or edited. In the common case, this isn't a problem, but certain problems cannot be solved without this information. In particular, these problems can't be solved with Django validators:
Consider this partial implementation of a Blog Article model that has URLs like this "/2008-3-11/my-article/" which allows articles to have the same title (actually, the same slug) so long as they are published on different dates:
#...
class BlogArticle(models.Model):
title = models.CharField(
'Name',
max_length = 100,
)
slug = models.CharField(
'Slug',
max_length = 100,
editable = False,
)
published = models.DateField(
'Date',
)
# more fields here...
class Admin:
# options...
pass
class Meta:
unique_together = (('slug', 'published',),)
def save(self):
from django.template.defaultfilters import slugify
self.slug = slugify(self.title)
super(BlogArticle, self).save()
def get_asbolute_url(self):
p = self.published
return "/%d-%d-%d/%s/" % (p.year, p.month, p.day, p.slug,)
#...
This model has a "slug" field that gets populated from the "title" field in the "save()" method. This seems okay at first glance, but consider what would happen if a user entered these two articles in the admin:
title = "My Article"
published = "2008-3-11"
title = "my article"
published = "2008-3-11"
Despite the change in letter case, these articles will have the same slug and the same date, so the produced URLs won't be unique. Unfortunately, the "unique_together" constraint will not help us here in the admin, because "slug" is not calculated until "save" is called, which happens after all validators have run. This situation will produce a database exception, rather than a validation error, resulting in an ugly and confusing user experience. This little experiment also shows that adding an additional "unique_together" constraint for ('title', 'published',) would also be ineffective, because we could still get non-unique URLs from two similar titles with letter case changes or other subtle changes.
To solve this properly, we'll need to write a validator for the "title" field that checks whether the slugified version of the title is actually unique for the given date. It should look something like this:
def validate_title(field_data, all_data):
# do nothing if 'title' or 'published' didn't pass initial validation
if 'title' not in all_data or 'published' not in all_data:
return
# calculate the slug and published date and check if another article with the
# same slug and published date already exists
from django.template.defaultfilters import slugify
from datetime import date
slug = slugify(field_data)
published = date(*[int(d) for d in all_data['published'].split('-')])
if BlogArticle.objects.filter(slug = slug, published = published).count():
# error ???
pass
This looks almost right. It will work perfectly when we're creating a new BlogArticle, but this will prove to be a miserable bug when editing an existing BlogArticle, because we'll be forced to change the blog title or publish date to get the validation to work. To correct this logic, the validator needs access to the BlogArticle object being edited. Suppose the validator has access to this object, call it "original_object," which will be set to None when we are creating an object. Then the validator would look like this:
def validate_title(original_object, field_data, all_data)
# do nothing if 'title' or 'published' didn't pass initial validation
if 'title' not in all_data or 'published' not in all_data:
return
# calculate the slug and published date
from django.template.defaultfilters import slugify
from datetime import date
slug = slugify(field_data)
published = date(*[int(d) for d in all_data['published'].split('-')])
# we need to check for unqiueness if we are creating a new object or
# the existing object has changed
if not original_object or original_object.slug != slug or original_object.published != published:
if BlogArticle.objects.filter(slug = slug, published = published).count():
raise ValidationError("A blog article with a similar title was already published on this date. Please change the title.")
This will accomplish the validation we need, but there's a big problem here: how do we figure out the "original_object?" We could try to query for an existing object based on some data in "all_data," but that would be a fallacy, because "all_data" is being edited by the user and may no longer match an existing object in the database. We know that the admin view code must be aware of whether an object is being added or edited. If you take a quick look in the admin view functions (django.contrib.admin.views.main), you'll see an "add_stage" and a "change_stage" function. These functions instantiate an object of your model class' AddManipulator or ChangeManipulator, respectively, to accomplish the adding or editing process. The ChangeManipulator object stores the object being edited in its "original_object" attribute, and the AddManipulator has no such attribute. We need to find a point to hook into the admin / field / manipulator code to gain access to this "original_object" so we can pass it to our validator.
The validation occurs after POST when the manipulator object's "get_validation_errors" function is called. This function calls "get_validation_errors" for each oldforms.FormField object in the manipulator (the manipulator's "fields" are all objects that subclass django.oldforms.FormField). The oldforms.FormField's "get_validation_errors" function calls each validator function in its "validator_list" attribute, passing each function "field_data" and "all_data," the value of the current field being processed and all the POSTed values, respectively. This is a clean mechanism for handling validation, but the oldforms.FormFields' "get_validation_errors" functions are still hopelessly unaware of the current object being edited.
The trick that I've discovered is to hook into the code at the point where the manipulator's oldforms.FormField objects are created. At this point in the code, the "validator_list" can be modified with more powerful validators. When the admin views construct an AddManipulator or ChangeManipulator object, the manipulator constructor creates an oldform.FormField object for each field in your model class (for your reference, the model class fields inherit from django.db.models.fields.Field). The manipulator constructor accomplishes this by calling each model fields' "get_manipulator_fields" function, passing itself to each function as a parameter. The "get_manipulator_fields" function creates the oldforms.FormField object and, of course, sets its "validator_list" from the corresponding attribute stored in the model field object. At this point, we can hook in and modify the "validator_list" before the framework creates the oldforms.FormFields. I suggest doing this by subclassing a model field class. For django.db.models.fields.CharField, it looks like this:
class ContextValidatedCharField(models.CharField):
def __init__(self, context_validators = [], *args, **kwargs):
# call the superclass constructor
super(ContextValidatedCharField, self).__init__(*args, **kwargs)
# keep track of the original validator list
import copy
self._orig_validator_list = copy.deepcopy(self.validator_list)
# context_validators can be a single function or a list of functions
if type(context_validators) is not list:
context_validators = [context_validators,]
self.context_validators = context_validators
def get_manipulator_fields(self, opts, manipulator, change, name_prefix = '', rel = False, follow = True):
# pass on original_object information to the custom validator(s)
from django.utils.functional import curry
new_validators = []
for validator in self.context_validators:
# convert the three-parameter validator into a two-parameter function by
# currying in the original_object as the first parameter
new_validators.append(curry(validator, getattr(manipulator, 'original_object', None)))
self.validator_list = self._orig_validator_list + new_validators
# just use the framework, which will incorporate our modified validator_list
return super(ContextValidatedCharField, self).get_manipulator_fields(opts, manipulator, change, name_prefix, rel, follow)
# we need this to make the field behave correctly
def get_internal_type(self):
return "CharField"
Now, in our BlogArticle model, we'll change the "title" field to this:
title = ContextValidatedCharField(
verbose_name = 'Name',
context_validators = [validate_title,],
max_length = 100,
)
Note the new parameter "context_validators" that accepts our modified validate_title validator function. Now, slugs will be safely created from the title field and all blog articles will have unique URLs.
Now, what about preventing infinite loops in self-referencing foreign keys? As a concrete example of this problem, consider this: We want to build a model representing a simple website's navigation system. We'll create a NavigationNode model that stores each node's parent NavigationNode (with top-level nodes having no parent) and information about the URL and page to display. A stripped-down version of the model might look like this:
class NavigationNode(models.Model):
parent = ContextValidatedForeignKey(
'self',
verbose_name = 'Parent Node',
context_validators = [validate_parent,],
blank = True,
null = True,
)
slug = models.SlugField(
'Slug',
)
full_path = models.CharField(
max_length = 255,
editable = False,
)
page = models.ForeignKey(
FlatPage, # assume that we have a model called FlatPage
verbose_name = "Flat Page",
)
# more fields here...
class Admin:
pass
def save(self):
# store the full path for easy URL lookups and other performance optimizations
if self.parent:
self.full_path = self.parent.full_path + self.slug + '/'
else:
self.full_path = '/' + self.slug + '/'
super(NavigationNode, self).save()
def get_absolute_url(self):
return self.full_path
Here, we've used a new field subclass called "ContextValidatedForeignKey" that works similarly to the "ContextValidatedCharField" described above. Its definition looks like this:
class ContextValidatedForeignKey(models.ForeignKey):
def __init__(self, to, context_validators = [], *args, **kwargs):
# call the superclass constructor
super(ContextValidatedForeignKey, self).__init__(to, *args, **kwargs)
# keep track of the original validator list
import copy
self._orig_validator_list = copy.deepcopy(self.validator_list)
if type(context_validators) is not list:
context_validators = [context_validators,]
self.context_validators = context_validators
def get_manipulator_fields(self, opts, manipulator, change, name_prefix = '', rel = False, follow = True):
# pass on context information to the custom validator(s)
from django.utils.functional import curry
new_validators = []
for validator in self.context_validators:
new_validators.append(curry(validator, getattr(manipulator, 'original_object', None)))
self.validator_list = self._orig_validator_list + new_validators
return super(ContextValidatedForeignKey, self).get_manipulator_fields(opts, manipulator, change, name_prefix, rel, follow)
def get_internal_type(self):
return "ForeignKey"
Finally, we need to implement the validator:
def validate_parent(original_object, field_data, all_data):
# if previous validation of parent or slug failed, skip
if 'parent' not in all_data or 'slug' not in all_data:
return
# get the parent and slug values
slug = all_data['slug']
if field_data:
parent_id = int(field_data)
else:
parent_id = None
# if we're creating a new object or the existing object has changed, we need to verify
# that the URL will be unique
if not original_object or original_object.parent_id != parent_id or original_object.slug != slug:
# first check if the slug is okay here
if parent_id:
if NavigationNode.objects.filter(parent__id = parent_id, slug = slug).count():
raise ValidationError("Another node already uses this URL. Please change the slug.")
else:
if NavigationNode.objects.filter(parent__isnull = True, slug = slug).count():
raise ValidationError("Another root node already uses this URL. Please change the slug.")
# next, we need to check for an infinite loop
# if we're editing an object, we need to verify that the object doesn't exist anywhere
# in its ancestor path
if original_object and parent_id:
p_id = parent_id
while p_id:
# try - just in case
try:
parent = NavigationNode.objects.get(pk = p_id)
except:
parent = None
if parent and original_object.id == parent.id:
raise ValidationError("Recursive path detected! This node cannot be in its own parent path.")
# move up the path
p_id = parent.parent and parent.parent.id or None
And that's it. An attempt to solve a similar problem was written up on djangosnippets. The author wrote a standard validator, but the code falls short of solving the problem, because it assumes that the slug field does not change. The code is still prone to producing an infinite loop.
Clearly, this "ContextValidatedField" method is a bit of a hack. A better solution might be to modify the Django framework to pass the original object data to all validator functions, preferably in a backwards-compatible way. One obvious problem with that approach is that validator functions could do perverse things like delete the original object. Maybe a better solution would be to have the framework pass another dictionary called "old_data" or "original_data" as a third parameter to each validator function that would contain all of the original object's data but would provide no mechanism to alter the original object.
I wasn't able to find any TRAC tickets or other information with a quick search of djangoproject.com, and I think this problem may merit opening a ticket. I welcome any comments or ideas on how to solve this problem more cleanly or easily in Django.
If you've ever needed to use the Django database API or templating system from a standalone Python script, you've probably written a quick workaround or ugly hack to get the Django environment working properly. Specifically, this problem has to be solved in order to run Django scripts as cron jobs. Some possible solutions to this problem were written up by James Bennet and Scott Newman. I prefer the clean approach in this method documented by Jared Kuolt.
Jared uses an undocumented function in django.core.management called setup_environ to prepare a script so that Django can be used normally, just like you would use it in any of your website's view modules. It goes like this:
from django.core.management import setup_environ
import settings
setup_environ(settings)
The setup_environ function simply takes your project's settings module as a parameter and sets up Django accordingly. Search for this function in django/core/management.py if you want to see the details of how Django sets itself up. Though this method is simple, elegant, and decently abstracted, it has two main limitations. It may only be a three-line incantation, but it's a pain to memorize these sort of things, and it would really be ideal if this could be accomplished in one line. More importantly, in order for this method to work, your script's file must live in the root of your project, so that "settings.py" is its neighbor. I prefer to keep my cron job scripts or data importing scripts in an appropriately named subdirectory, such as "scripts." From a subdirectory, import settings will not work.
I've written a script to solve this problem so that you only need to write one line of code that's easy to memorize. You'll need to copy and unzip this script, setup_django.py, into the directory where your script lives. Write this as the first line of your script:
import setup_django
And voila! That's all you need to do. You can now use the Django database API just as you'd expect. The code borrows some ideas directly from the setup_environ function in django.core.management. It goes like this:
import os, sys, re
# stop after reaching '/' on nix or 'C:\' on Windows
top_level_rx = re.compile(r'^(/|[a-zA-Z]:\\)$')
def is_top_level(path):
return top_level_rx.match(path) is not None
def prepare_environment():
# we'll need this script's directory for searching purposess
curdir, curfile = os.path.split(os.path.abspath(__file__))
# move up one directory at a time from this script's
# path, searching for settings.py
settings_module = None
while not settings_module:
try:
sys.path.append(curdir)
settings_module = __import__('settings', {}, {}, [''])
sys.path.pop()
break
except ImportError:
settings_module = None
# have we reached the top-level directory?
if is_top_level(curdir):
raise Exception("settings.py was not found in the script's directory or any of its parent directories.")
# move up a directory
curdir = os.path.normpath(os.path.join(curdir, '..'))
# set up the environment using the settings module
from django.core.management import setup_environ
setup_environ(settings_module)
prepare_environment()
When you import this module, it runs prepare_environment. First, the function calculates its own path. It then takes this path and moves upward one directory at a time, attempting to import a module named "settings." At each step, it checks if it has reached the root directory using the is_top_level function, which employs a simple regular expression for detecting the root directory on Unix or Windows. If the root has been reached, an exception is raised; otherwise, when the settings module is found, it is passed to Django's setup_environ function.
Some may not find this method to be as nice as others, because you'll need to copy the "setup_django.py" script into all of your script directories, but the one-line import setup_django solution is very nice. The main caveat to consider is that if you have a file named "settings.py" other than the project's main "settings.py" in the directories above your script's path, this method will not work. This was only tested in Python 2.5.x, but there shouldn't be any problems running it in slightly older versions of Python.
Enjoy!
When clients come to our firm for interactive recommendations they
generally have a lot of ideas. The interactive world is scary. There are a lot
of acronyms. There are nerds that keep information shrouded in secrecy so that
you will have to continue to rely on them. Or worse, there are nerds that complicate
things by overexplaining things you don’t need to know.
As a result, clients try to get up to speed on current Internet trends or hide behind their IT person.
The first thing they will try to tackle and understand is the development language. Clients
will often agonize over the language used to develop their site, but this is
the wrong focus. Technology is going to change. Developers are smart and they
will figure out a way to make new technology work with what you have
established. If you build a site
and continue to pay attention to the backend as well as the front end, you are
going to make a long lasting Web site that will have a longer shelf life. If you build a site and don’t touch it again for 18 months, you
are going to have to rebuild it the next time you touch it. So why agonize over the language? Trust the experts.
The most important thing you can do as a client is decide what function your Web site will perform. Is it a brochure site? Is it an application? Is it core to your company's business?
There are plenty of companies out there that don’t need to create anything more than a brochure site. We don’t all need social networking sites.
The clients that are most successful are focused on the results, not the way we get there.
Instead of blowing all of your money on a redesign and then doing it again 18-48 months later, spend your money wisely. Invest as you go along. Pay attention to your analytics. Find an expert that is going to keep you up on technology without boring you with the details. You don’t care if Django or Python is the best solution, but you do care that users are finding the right information on the site.
This approach is not always applicable to large companies. When your Web site has to fit into a large-scale enterprise solution, it is very important to perform the due-dilligence to ensure the technologies you invest in will be supported later by your in-house technology team.
As an interactive professional for the past 10 years, I have seen a lot of languages and development theories come and go. Don’t agonize about the language, you’ll always be able to find someone to support it. Instead, focus on the content and the user experience.
Marketing is all about relating to humans. One way to
truly be a step ahead is to take the budget you have for your redesign, double
it and spend the second half on committed maintenance on your site. It will
guarantee a longer lifespan and will help you focus on customers- the reason
you made the site.