Thursday, December 3, 2009

Django settings.py

Today I accidentally deleted a settings.py file for a project I was working on. I google'd like crazy to try to find a place where I can get a copy of the default settings.py file to start rebuilding the settings file. I could not find one, so I had to go through the trouble of starting a new app, and then extracting the settings.py file from it.

To all you people who find yourselves in the same predicament that I was in today, here is the whole settings.py file in it's full glory. It was generated by Django 1.1.1.


# Django settings for ff project.

DEBUG = True
TEMPLATE_DEBUG = DEBUG

ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)

MANAGERS = ADMINS

DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = '' # Or path to database file if using sqlite3.
DATABASE_USER = '' # Not used with sqlite3.
DATABASE_PASSWORD = '' # Not used with sqlite3.
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.

# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/Chicago'

# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'

SITE_ID = 1

# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True

# Absolute path to the directory that holds media.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = ''

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = ''

# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/'

# Make this unique, and don't share it with anybody.
SECRET_KEY = '&52lnelj34-*+76c2x*e0q4&b9zec%@^l_@s%*u*ar!%b5v!yr'

# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.load_template_source',
'django.template.loaders.app_directories.load_template_source',
# 'django.template.loaders.eggs.load_template_source',
)

MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
)

ROOT_URLCONF = 'ff.urls'

TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
)

INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
)

Wednesday, November 11, 2009

Passwordless SSH made easy

Every six months, Ubuntu comes out with a new version. Each time that happens I always end up doing a fresh install, rather than taking the chances of an upgrade gone wrong. This means that every six months, I have to set up certain things all over again. The only thing I keep from one install to the next is my Firefox profile, and my "~/home/me/pictures" directory.

One of the things that always gets me is setting up SSH keys so I can log into my various shells without needing to type a password. So instead of this:

chris@chris-comp:~$ ssh chris@myshell.com
password for chris@myshell.com:
chris@myshell:~$


I can just do this:

chris@chris-comp:~$ ssh myshell
chris@myshell:~$


How to do it



First lets add a ssh "bookmark". Add the following to a file called "config" in your .ssh directory:

chris@chris-comp:~$ vim ~/.ssh/config
...


Host myshell
User chris
HostName myshell.com



This basically sets up an alias that lets you use "myshell" in place of "chris@myshell.com". This alias works anywhere on the system, including scp:

chris@chris-comp:~$ scp ~/myfile.txt myshell:~/



If you use Ubuntu, you can go to Places -> Connect to Server, then select "ssh" in the dropdown menu. Under server, just enter "myshell". This will allow you to browse the contents of that ssh machine in Nautilus! Pretty cool! Now you can drag and drop stuff from your ssh machine into apps on your desktop!

Creating the keys


Now, lets create our keys, run the ssh-keygen command:

chris@chris-comp:~$ ssh-keygen -t rsa -C "me@email.com"
Generating public/private rsa key pair.
Enter file in which to save the key (/home/chris/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/chris/.ssh/id_rsa.
Your public key has been saved in /home/chris/.ssh/id_rsaub.
The key fingerprint is:
01:0f:f4:3b:ca:85:d6:17:a1:7d:f0:68:9d:f0:a2:db me@email.com


This command basically creates both a private key and a public key for you, and places it in the /.ssh directory in your home directory. The idea is that you place the public key onto any server's you want to connect to. When you connect to that server, it will check the public key you gave it against your private key. If it matches, you're let in. Otherwise it'll ask you for a password.

Now that we've created the keys, lets add them to our server. Googling around the internet brings up a lot of ways to do this, but by far the easiest is this:

chris@chris-comp:~$ ssh-copy-id -i .ssh/id_rsa.pub myshell


This command will copy your public key over to the machine's public key holder place (.ssh/id_rsa.pub).

If all went well, you can connect to your ssh host without needing a password:

chris@chris-comp:~$ ssh myshell
chris@myshell:~$


Yay!

Saturday, October 17, 2009

"View on site" links in list view with Django Admin

The Django admin application has this feature where when you click on an object, if that Model has a "get_absolute_url" method defined, in the corner of the page there will be a link that will go to that object on your site:



This is really handle, but how can I get that link to be put in the list view, so I don't have to click on the object first to get to that link? The answer is to define the following function in the model:


def adminlink(self):
link = self.get_absolute_url()
url = "http://example.com%s" % link
link = "<a href='%s' target='_blank'>Link</a>" % url
return link
adminlink.allow_tags = True


Make sure you include the last line. You must set the function attribute "allow_tags" in order for the admin to not escape the '<' and '>' characters.

The admin declaration, should look like this:


class MyAdmin(admin.ModelAdmin):
list_display = ('user', 'field1', 'field2', MyModel.adminlink)

admin.site.register(MyModel, MyAdmin)


Now when you look at the admin page where it lists all object of MyModel, there should be a new column with your links in it:


Sunday, September 27, 2009

Deploying Django to replace a PHP site

From October 2007 to May 2008 I developed a webapp in PHP. Over the past year and a half since it's been up, I've added a few features to it, and it's slowly grown in popularity. A few months ago I came to the conclusion that the site need a total rewrite in order for the code to move forward.

After doing some research, I decided on Django, since just about everything I've read about the framework has been positive.

Fast forward to now. The Django version of the site is about 90% done. It uses way less code, and does everything the PHP site did and then some. Plus, it really only took me 2 months to do the Django version, whereas it took my about 6 or 7 to do it in PHP. Some of that has to do with Django's tools, but mostly the reduced time is attributed to me being a better programmer, as well as the overall design of the site already being determined.

Deploying

Now I must somehow figure out the best way to replace all the PHP code with the Django code. Tjis is not an easy task. The Django version of the site has a completely different authentication method than the PHP version. It's not as easy as jut migrating all data from the PHP database, to the Django database. (my PHP site uses MySQL and the Django version of my site uses PostGIS)

My Plan

This is what I have decided to do: Right now I have the old PHP version of the site located at domain.com, and the Django version located at beta.domain.com. I want to rename the PHP domain to old.domain.com, and then rename the Django url to plain ol' domain.com. Also I am going to have it so if someone tries to visit a link such as "domain.com/page.php", it will detect it's a link to the old site (due to the presence of ".php" in the URL), then redirect that request to "old.domain.com/page.php".

Mod_rewrite, right?

There are a few ways to do it. The first way that pops into the mind of a web developer is to create a new mod_rewrite rule. Mod_rewrite is a well-known apache module that handles rewriting URL's. I have never really gotten used to using mod_rewrite, mostly because theres no real way (that I know of) to test rules without breaking your site.

My Solution

After thinking about the problem for a few days, I came up with a pretty simple and convenient way to do the redirection all within Django. This is how:

First, point the following domains to your django app: domain.com, and beta.domain.com. Point old.domain.com to the PHP app. If you're using webfaction for your host (and you should if you're wanting to deploy a django app on a shared hosting budget), then this is really easy. Just go to the control panel and point your subdomains to the proper application profiles. If you're not using webfaction, then you'll have to mess around with your apache's httpd.conf settings.

Secondly, add a new app within your django project called "redirect". Inside that new app, create a new view called "redirect". That view should look like this:


from django.http import HttpResponseRedirect

def redirect(request):
"""redirect this request to the old domain
(the PHP version)
"""

return HttpResponseRedirect("http://old.domain.com" + request.get_full_path())


Basically what this does is redirect all requests to a new domain. We want to send all requests trying to get a ".php" file through this view. We achieve this by adding the following to urls.py:


(r'\.php', "redirect.views.redirect", ),


For best results, put this urlpattern at the very bottom of the list. This urlpattern matches any url that contains ".php" anywhere in it.

And thats it. I personally haven't deployed this method completely yet, as my Django site still has a few weeks of testing to go through before it's sent out to the public. Once I get it out there, I'll edit this post with any problems or workarounds I encountered.

Tuesday, September 22, 2009

Cron jobs with django made easy

The Problem:

I run a Django website. Its a pilot logbook application, where pilots can use the site to keep track of their flying experience. Each flight you enter to your logbook is represented by the Flight model. The Flight model contains all the information about the flight, such as comments about the flight, length of the flight, when the flight took place, and the route of the flight. The route of the flight is represented by the Route model, which is attached to the Flight model by a ForeignKey relation. The way the user logs a route is by entering a string in to the route field of the FlightForm such as "KLGA - KBOS - KLGA", which represents a flight from La Guardia to Boston, then back to La Guardia. When the FlightForm is saved, the string gets translated to a Route instance via a class method I created. Adding the route to the flight looks a little like this:


r=Route.from_string("KLGA-KBOS-KLGA")
flight.route = r
flight.save()


The problem is that the Route object is fairly complex. It contains a few fields that represent pre-rendered HTML, as well as RouteBase instances (in this case three, two KLGA's, and a KBOS). The RouteBase object then is connected to an Airport instance, which stores the airport's name, city, and coordinates. Since the Route object needs to have foreign key relations for it to make sense (routes with no airports doesn't make sense), the route object has to be saved to the database before the RouteBase's can be added.

What ends up happening here, is that whenever a flight is edited, whether or not the route is edited, a new route instance is created, and the old one is just discarded. Or if the user wants to delete a flight, the route object remains as well. Over time, orphaned routes start to build up, and may cause queries to be slower that they otherwise could be.

The Solution:

The solution here is very quite easy. Create a function like this:


def delete_empty_routes():
from route.models import Route
Route.objects.filter(flight__isnull=True).delete()


...and run it once a day.

But how should we do this? There are a few ways. Some are easy, and some are hard. The easiest way (well, the way I do it) is to make this function a view:


def delete_empty_routes(request):
from route.models import Route
from django.http import HttpResponse
qs = Route.objects.filter(flight__isnull=True)
c = qs.count
qs.delete()
return HttpRequest("deleted %s Routes" % c,
mimetype="text/plain")


and whenever you want to clear out all empty routes, attach this view to a URL, then just hit the URL. For automation, you can easiely add this to your crontab:


6 30 * * * wget http://domain.com/delete-empty-routes


...which will hit that URL every day at 6:30 AM and clear out all empty routes. Another Problem: My site also has a feature where the user can elect to have an email sent to them every few days or weeks with a zipped CSV file attached that contains all their flights they have logged with the site. In the aviation world, your logbook data is very important. If you lose all that data you are screwed. A lot of my users are apprehensive about storing their data with me, fearing that the site will go down one day, taking their data with it. Once I implemented the email feature, I saw my signups skyrocket.

Another Solution:

Again, the solution to this problem is to create a view function that emails each user a backup of their data:


def email_all_users(request, interval):
#interval = an int representing weekly/monthly/biweekly
profiles = Profile.objects.filter(backup_freq=interval)
for p in profiles:
email_backup(p.user) #create backup then send to user
return HttpResponse("success!")


...then set it to a URL, and then to a crontab:


30 3 1 * * wget http://domain.com/send_email-1
30 3 7 * * wget http://domain.com/send_email-2
30 3 14 * * wget http://domain.com/send_email-3
30 3 21 * * wget http://domain.com/send_email-4


One last Problem:

Now you successfully have a URL on your site that, when hit, will send an email to each user who has elected to receive backups. The problem now is that you have a URL on your site, that when hit, sends an email to each user. What is a search engine gets ahold of this URL? What if a user gets ahold of it? Your users will get spammed.

One last Solution:

To protect this function from being hit by unauthorized persons, we must edit the function a little bit:


def email_all_users(request, interval):
from settings import SECRET_KEY # make sure the secret key is present, or else fail
assert request.POST.get('key', "") == SECRET_KEY
#interval = an int representing weekly/monthly/biweekly
profiles = Profile.objects.filter(backup_freq=interval)
for p in profiles:
email_backup(p.user) #create backup then send to user
return HttpResponse("success!")


Now in your crontab, add the following:


30 3 1 * * wget http://domain.com/send_email-1?key=4f46g...
30 3 7 * * wget http://domain.com/send_email-2?key=4f46g...
30 3 14 * * wget http://domain.com/send_email-3?key=4f46g...
30 3 21 * * wget http://domain.com/send_email-4?key=4f46g...


If you want to be even more thorough, you can have the crontab call a python script which adds your settings file to the python path (if it isn't already), then import the SECRET_KEY that way, but doing it this way where you copy/paste the SECRET_KEY works too. We could also have it return a 404 error instead of just an assertion, but either way it still works.