Django Messaging for AJAX Calls Using Jquery

The messaging contrib app for Django has always been been tied to a user, which has prevented me from using it in any of my apps. Now that the Django 1.2 Alpha has been released, I’ve been able to play with it, and feel good about using it. My sites usually have a mix of AJAX and traditional HTTP requests. So figuring out a good solution to handle messages for the AJAX requests, while maintaining consistency in interacting with the API was important to me. Here’s my solution for handling messages in AJAX requests.

First we need a middleware that will detect if the request is an AJAX request. I always use JSON, but if you mix the data types, you might have to add some additional logic here. The middleware adds to the JSON any messages that we might have added in the view.

import simplejson as json

from django.contrib import messages

class AjaxMessaging(object):
    def process_response(self, request, response):
        if request.is_ajax():
            if response['Content-Type'] in ["application/javascript", "application/json"]:
                try:
                    content = json.loads(response.content)
                except ValueError:
                    return response

                django_messages = []

                for message in messages.get_messages(request):
                    django_messages.append({
                        "level": message.level,
                        "message": message.message,
                        "extra_tags": message.tags,
                    })

                content['django_messages'] = django_messages

                response.content = json.dumps(content)
        return response

Make sure to add the new middleware to settings.py.

'apps.main.middleware.AjaxMessaging'

So if we have a view that responds to an AJAX request, we can add a message the same way we would for a traditional HTTP response:

if success:
    messages.success(request, "The object has been modified.")
else:
    messages.error(request, "The object was not modified.")

We need a place to put the messages in our template:

<ul id="messages">
    {% for message in messages %}
    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
    {% endfor %}
</ul>

We’ll be using a global ajaxComplete handler, so we’ll have to use a JSON parser. Simply include it with the rest of our javascript. jquery.json.js

And finally the handler, which simply loops through all the Django messages, and fades them out after three seconds.

function addMessage(text, extra_tags) {
    var message = $('<li class="'+extra_tags+'">'+text+'</li>').hide();
    $("#messages").append(message);
    message.fadeIn(500);

    setTimeout(function() {
        message.fadeOut(500, function() {
            message.remove();
        });
    }, 3000);
}

$(document).ready(function() {
    $('#messages').ajaxComplete(function(e, xhr, settings) {
        var contentType = xhr.getResponseHeader("Content-Type");

        if (contentType == "application/javascript" || contentType == "application/json") {
            var json = $.evalJSON(xhr.responseText);

            $.each(json.django_messages, function (i, item) {
                addMessage(item.message, item.extra_tags);
            });
        }
    }).ajaxError(function(e, xhr, settings, exception) {
        addMessage("There was an error processing your request, please try again.", "error");
    });

How to Handle HTTP Redirects with Jquery and Django

Jquery has a funny way of handling 302 redirects during AJAX calls. These frequently come up when I need to redirect a user to a login page. Here’s how I’m handling it with Django.

The underlying problem we experience with redirects in Jquery is that the redirects are followed. So instead of getting a status code of HTTP/1.1 302 Found, we get HTTP/1.1 200 OK. So one way to get around this is to return a made up status code. This is easily accomplished with a middleware in django.

from django.http import HttpResponseRedirect

class AjaxRedirect(object):
    def process_response(self, request, response):
        if request.is_ajax():
            if type(response) == HttpResponseRedirect:
                response.status_code = 278
        return response

Make sure to add the new middleware to settings.py.

'apps.main.middleware.AjaxRedirect'

And the final piece of the puzzle is to add a global AJAX complete handler, that checks for the new status code, redirects the page to the Location header in the HTTP response, and replace the next query variable. The last part is important because the login_required decorator automatically sets the next query variable to the HTTP Referrer, which in this case would be the URL of the AJAX call. So we want to replace it with the page we’re currently on.

$(document).ready(function() {
    $('body').ajaxComplete(function(e, xhr, settings) {
        if (xhr.status == 278) {
            window.location.href = xhr.getResponseHeader("Location").replace(/\?.*$/, "?next="+window.location.pathname);
        }
    });
});

Setting Up Apache-PHP-Python-MySQL on Mac OS X Snow Leopard 10.6

  1. Download and install MySQL Package file [MySQL 5.1 for 10.5 (x86_64)]
  2. Install MySQL Startup Item [Howto]
  3. Turn on Web Sharing in System Preferences -> Sharing
    Screen shot 2009-11-29 at 5.19.43 PM
  4. Copy /etc/php.ini.default to /etc/php.ini
    sudo cp /etc/php.ini.default /etc/php.ini
  5. Add timezone information to php.ini (PHP will error without this)
    date.timezone = 'America/New_York'
  6. Install php-mcrypt
    1. Download libmcrypt 2.5.8
    2. Build and install .
      ./configure --disable-posix-threads --enable-dynamic-loading
      make
      sudo make install
    3. Download PHP 5.3.1
    4. Navigate to php-5.3.1/ext/mcrypt/
    5. Build and install .
      phpize
      ./configure
      make
      sudo make install
    6. Add extension to php.ini
      extension=mcrypt.so
  7. Install APC
    1. Download PCRE 8.0
    2. Build and install .
      ./configure
      make
      sudo make install
    3. Download APC 3.1.3p1
    4. Navigate to APC-3.1.3p1/APC-3.1.3p1/
    5. Build and install .
      phpize
      ./configure
      make
      sudo make install
    6. Add extension to php.ini
      extension=apc.so
  8. Download and install MySQL-python 1.2.3c1
    sudo python setup.py install
  9. Download and install libjpeg.v7
    ./configure --enable-shared
    make
    sudo make install
  10. Download and install Python Imaging Library 1.1.6
    sudo python setup.py install

MySQL Startup Item for Mac OS 10.6 Snow Leopard

Save the following as /System/Library/LaunchDaemons/org.mysql.mysqld.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>RunAtLoad</key>
	<true/>
	<key>Umask</key>
	<integer>7</integer>
	<key>UserName</key>
	<string>_mysql</string>
	<key>Disabled</key>
	<false/>
	<key>WorkingDirectory</key>
	<string>/usr/local/mysql</string>
	<key>GroupName</key>
	<string>_mysql</string>
	<key>KeepAlive</key>
	<true/>
	<key>Program</key>
	<string>/usr/local/mysql/bin/mysqld</string>
	<key>Label</key>
	<string>org.mysql.mysqld</string>
	<key>ProgramArguments</key>
	<array>
		<string>--user=_mysql</string>
	</array>
</dict>
</plist>

And in a bash session, run the following:

sudo chown root:wheel /System/Library/LaunchDaemons/org.mysql.mysqld.plist
sudo launchctl load /System/Library/LaunchDaemons/org.mysql.mysqld.plist
sudo launchctl start org.mysql.mysqld

Nginx Startup Script for Mac OS 10.6 Snow Leopard

If you don’t use MacPorts, and have a hard time finding a startup plist file for Nginx, and had trouble getting Nginx to startup with your custom plist file, this might be your solution:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>nginx</string>
    <key>Program</key>
    <string>/usr/local/nginx/sbin/nginx</string>
    <key>KeepAlive</key>
    <true/>
    <key>NetworkState</key>
    <true/>
    <key>LaunchOnlyOnce</key>
    <true/>
</dict>
</plist>

Just save this as /System/Library/LaunchDaemons/org.nginx.nginx.plist and perform the following in a bash session:

sudo chown root:wheel /System/Library/LaunchDaemons/org.nginx.nginx.plist
sudo launchctl load /System/Library/LaunchDaemons/org.nginx.nginx.plist
sudo launchctl start org.nginx.nginx

Growl Notifications for Apple Mail on Mac OS X Snow Leopard (10.6)

UPDATE: You can grab a beta version of 64-bit Growl 1.2 from the Growl beta site.

GrowlMail is broken on Mac OS X Snow Leopard (10.6). The dev team for Growl has a fix in the works, but I use Prowl and like to have notifications go to my iPhone when I’m not at my computer. So in the meantime, here is my solution.

Screen shot 2009-09-02 at 9.51.22 AM

I was originally inspired by James Higgs.

You first want to create a new AppleScript using AppleScript Editor and save it somewhere logical. I used /Library/Scripts/Mail Scripts/Rule Actions/Growl.scpt Below is the code:

-- Growl Alerts in Mail
-- Hunter Ford [http://www.cupcakewithsprinkles.com]
-- This script arises from the lack of any Growl Support in Mac OS X Snow Leopard (10.6)
-- Code inspired by and adapted from James Higgs [http://blog.jameshiggs.com/2009/08/28/growlmail-on-snow-leopard-a-temporary-fix/] as well as those mentioned.

tell application "GrowlHelperApp"
	-- Make a list of all the notification types
	-- that this script will ever send:
	set the allNotificationsList to {"New Email"}

	-- Make a list of the notifications
	-- that will be enabled by default.
	-- Those not enabled by default can be enabled later
	-- in the 'Applications' tab of the growl prefpane.
	set the enabledNotificationsList to {"New Email"}

	-- Register our script with growl.
	-- You can optionally (as here) set a default icon
	-- for this script's notifications.
	register as application "Mail" all notifications allNotificationsList default notifications enabledNotificationsList icon of application "Mail"
end tell

-- Mail Rule Trigger
--
-- Source: Benjamin S. Waldie [http://www.mactech.com/articles/mactech/Vol.21/21.09/ScriptingMail/index.html]
using terms from application "Mail"
	on perform mail action with messages theSelectedMessages for rule theRule
		repeat with thisMessage in theSelectedMessages
			-- Process the current message

			-- Grab the subject and sender of the message
			set growlSubject to subject of thisMessage
			set growlSender to my ExtractName(sender of thisMessage)

			-- Use the first 100 characters of a message
			set growlMessage to (content of thisMessage)
			set growlLength to (length of growlMessage)

			if growlLength > 100 then
				set growlMessage to (characters 1 through 100 of growlMessage) & "…"
			end if

			set growlMessage to growlSubject & "

" & growlMessage

			-- Send a Notification
			tell application "GrowlHelperApp"
				notify with name "New Email" title growlSender description growlMessage application name "Mail"
			end tell
		end repeat
	end perform mail action with messages
end using terms from

-- *ExtractName*
--
-- gathers the name portion from the "From: " line
--
-- Source: robJ [http://forums.macosxhints.com/archive/index.php/t-19954.html]
to ExtractName(sender_)
	if sender_ begins with "<" then
		return text 2 thru -2 of sender_
	else
		set oldTIDs to text item delimiters
		try
			set text item delimiters to "<"
			set name_ to first text item of sender_
			set text item delimiters to oldTIDs
		on error
			set text item delimiters to oldTIDs
		end try
		return name_
	end if
end ExtractName

Run this first in the AppleScript Editor. As this is what will “register” the notification with Growl. Click the green “play” button that says “Run”.

Screen shot 2009-09-01 at 10.46.18 PM

Then you want to create a new rule in mail that will run this script on every message that comes in.

Screen shot 2009-09-01 at 4.45.20 PM

Make sure to select “Don’t Apply” that way you don’t flood your screen with tons of Growl notifications.

Screen shot 2009-09-01 at 4.53.15 PM

Digitally Signed E-Mails with Thawte Personal E-Mail Certificates

UPDATE: Thawte is doing away with Free Personal E-Mail Certificates. You can read their FAQ for more information.

Digitally signed and encrypted e-mails have been around for a long time. Unfortunately, it’s never really taken off. Thawte hopes to change all that with its Web of Trust. They offer free certificates for personal use.

The biggest problem to date in my experience has been e-mail clients ability to handle digitally signed and encrypted e-mails. I use a Apple Mail to handle all of communications since its support for IMAP is top-notch.

To give you a little background, digital signatures are apart of the S/MIME specification. Here’s how Wikipedia defines it.

S/MIME provides the following cryptographic security services for electronic messaging applications: authentication, message integrity and non-repudiation of origin (using digital signatures) and privacy and data security (using encryption). S/MIME specifies the application/pkcs7-mime (smime-type “enveloped-data”) type for data enveloping (encrypting): the whole (prepared) MIME entity to be enveloped is encrypted and packed into an object which subsequently is inserted into an application/pkcs7-mime MIME entity.

If you want to get started using digital signature in your e-mail, you need to first sign-up with Thawte. At the bottom of the page, click the link that says “Click here”. You’ll then have to proceed through the wizard, verify your email, and request a certificate. After your certificate has been generated, you’ll download it, and it should get added to your keychain.

After it’s been added to your keychain, you need to make one adjustment so that you can use it in Mail. You’ll want to double-click the certificate you just added so that you get a new pane. Then, click “Trust” to expand it. Then you need to tell it to “Always Trust” “When using this certificate”.

Screen shot 2009-08-19 at 4.01.10 PM

The next time you open a composition window in Mail, you’ll see a few new buttons whenever you compose an email from the email address you got the certificate for.

Screen shot 2009-08-19 at 4.11.14 PM

The following is from Mail’s help:

A Signed icon (containing a checkmark) in the lower-right side of the message header indicates the message will be signed when you send it.

An Encrypt (closed lock) icon appears next to the Signed icon if you have a personal certificate for a recipient in your keychain; the icon indicates the message will be encrypted when you send it.

You can add several emails to your Thawte account, but each one will need its own certificate.

Now you’re halfway there. You need to become “Trusted” so that your name can be put in the certificate. Upon doing this, and downloading new certificates, your recipients won’t be presented with the message: “Unable to verify message signature”. So in order to become trusted, your identity must be verified by a Web of Trust notary. Go to the website and login. You’ll have to locate a few notaries in your area that will verify your information. Each notary can assign a certain number of “trust points”. You need 50 trust points in order to be trusted. Each notary can give out between 10-35 trust points. You’ll have to meet this person face-to-face and present them with a few documents (driver’s license, passport, company photo ID) and copies of those documents for them to keep. Before you’re meeting you’ll have to share your details with them through the website.

Half-Off Depot ‘Notify When Back In Stock’

Half-Off Depot is great! You can find a lot of restaurants or services and get gift cards or certificates for half off the actual price. No gimmicks. One problem is that everything sells out so fast! And I never know when things come back in stock. There’s a “feature” where you can be notified when things come back in stock, but it just doesn’t work. So… to solve the problem I came up with a little script that will scrape the site for the different gift cards I’m looking out for at specified intervals and send me an email. It’s not perfect, but it does the trick for now.

#!/bin/bash

NUM_AVAILABLE=`curl -s http://halfoffdepot.com/atlanta/categories/restaurants/all-restaurants/5-seasons-brewing-company.html | grep '</h1></span>' | sed -e 's/^.*;">//' -e 's/<\/.*$//'`

if [ $NUM_AVAILABLE -ne 0 ]
then
    echo "http://halfoffdepot.com/atlanta/categories/restaurants/all-restaurants/5-seasons-brewing-company.html" | mail -s "There are $NUM_AVAILABLE gift cards available for 5 Seasons." "Your Name <user@domain.com>"
else
    echo "No gift cards available for 5 Seasons." && echo
fi

You can just replace the link with the one you’re trying to target, and replace your email address. I have this running as a cron job daily at 8am,12pm, and 6pm. It’s not perfect because the script doesn’t check for a duplicate email, but things tend to sell-out fast, so I only usually get 2-3 emails

0 8,12,18 * * * /scripts/halfoff > /dev/null 2>&1

Better Weather Script for Indigo

I find the weather script that comes with Indigo a bit lacking, and at times a little slow. Also, I prefer using the weather feed from Yahoo since it matches up to the weather widget in my dashboard.

You’ll first need to get the right parameter for your local weather. Go to Yahoo Weather and type in your city/zip code. Just look at the URL and you should have something like: http://weather.yahoo.com/forecast/USGA0028.html

Grab what comes after ‘forecast/’ and before ‘.html’ and replace ‘USGA0028′ in the script below.

property fetchTimeout : 15 -- don't wait for query response for more than 15 seconds...

try
    with timeout of fetchTimeout seconds
        do shell script "curl -s 'http://xml.weather.yahoo.com/forecastrss?p=USGA0028' | grep -E '(<yweather:condition|/\\>)' | sed -e 's/^.*temp=\"//' -e 's/\".*$//'"
        set temperature to result

        do shell script "curl -s 'http://xml.weather.yahoo.com/forecastrss?p=USGA0028' | grep -E '(<yweather:condition|/\\>)' | sed -e 's/^.*text=\"//' -e 's/\".*$//'"
        set conditions to result

        do shell script "curl -s 'http://xml.weather.yahoo.com/forecastrss?p=USGA0028' | grep -E '(<yweather:forecast day=\"|/\\>)' | sed -e 's/^.*high=\"//' -e 's/\".*$//' -e 'q'"
        set high to result

        do shell script "curl -s 'http://xml.weather.yahoo.com/forecastrss?p=USGA0028' | grep -E '(<yweather:forecast day=\"|/\\>)' | sed -e 's/^.*low=\"//' -e 's/\".*$//' -e 'q'"
        set low to result

        tell application "IndigoServer"
            if not (variable "weatherTemperature" exists) then
                make new variable with properties {name:"weatherTemperature", value:temperature}
            else
                if value of variable "weatherTemperature" is not temperature then
                    set value of variable "weatherTemperature" to temperature
                end if
            end if

            if not (variable "weatherConditions" exists) then
                make new variable with properties {name:"weatherConditions", value:conditions}
            else
                if value of variable "weatherConditions" is not conditions then
                    set value of variable "weatherConditions" to conditions
                end if
            end if

            if not (variable "weatherTemperatureHigh" exists) then
                make new variable with properties {name:"weatherTemperatureHigh", value:high}
            else
                if value of variable "weatherTemperatureHigh" is not high then
                    set value of variable "weatherTemperatureHigh" to high
                end if
            end if

            if not (variable "weatherTemperatureLow" exists) then
                make new variable with properties {name:"weatherTemperatureLow", value:low}
            else
                if value of variable "weatherTemperatureLow" is not low then
                    set value of variable "weatherTemperatureLow" to low
                end if
            end if
        end tell
    end timeout
end try

Adjusting the Climate upon Leaving and Restoring Previous Settings upon Return

When you’re not at home, you generally don’t need to have the A/C running at full blast. One simple solution for this is turn down your A/C when you leave, and turn it back up when you return. Using Indigo and a Insteon compatible thermostat, like the one from Venstar, make this a breeze. My original solution to this worked, but it didn’t take into account my overrides of the thermostat. I’ve added a few variables to Indigo to save the thermostats current state when I leave, and restore it back to that state once I’ve returned.

For this to work, we’re going to need to two triggers (which you may already have setup), and three action groups.

First, we need a trigger to set off the action group to save the settings (you could very well embed the action script into your trigger, but I usually modularize my scripts so that I can perform actions from a remote or from multiple triggers). I have a trigger that is called when I hit the “All Off” button on a KeypadLinc by my front door. I have another trigger that will restore my settings when I return; it is set off by a motion detector by my front door.

You’ll need to create a few variables first: climateHeatSetpointPrevious, climateCoolSetpointPrevious, climateHvacModePrevious, climateFanModePrevious, climateHeatSetpointAway, climateCoolSetpointAway, climateHvacModeAway, climateFanModeAway

Now for the action groups:

1. “A/C – Save Current Settings”

if heat setpoint of device "Thermostat" is not value of variable "climateHeatSetpointAway" or cool setpoint of device "Thermostat" is not value of variable "climateCoolSetpointAway" or hvac mode of device "Thermostat" is not value of variable "climateHvacModeAway" or fan mode of device "Thermostat" is not value of variable "climateFanModeAway" then
    set value of variable "climateHeatSetpointPrevious" to heat setpoint of device "Thermostat"
    set value of variable "climateCoolSetpointPrevious" to cool setpoint of device "Thermostat"
    set value of variable "climateHvacModePrevious" to hvac mode of device "Thermostat"
    set value of variable "climateFanModePrevious" to fan mode of device "Thermostat"
end if

*Edit: I added the if statement so that if I hit the ‘All Off’ button twice by mistake, it wouldn’t save my “away” settings as the “previous” settings.

2. “A/C – Set Away Settings”

set heat setpoint of device "Thermostat" to value of variable "climateHeatSetpointAway"
set cool setpoint of device "Thermostat" to value of variable "climateCoolSetpointAway"
set hvac mode of device "Thermostat" to value of variable "climateHvacModeAway"
set fan mode of device "Thermostat" to value of variable "climateFanModeAway"

3. “A/C – Restore Previous Settings”

set heat setpoint of device "Thermostat" to value of variable "climateHeatSetpointPrevious"
set cool setpoint of device "Thermostat" to value of variable "climateCoolSetpointPrevious"
set hvac mode of device "Thermostat" to value of variable "climateHvacModePrevious"
set fan mode of device "Thermostat" to value of variable "climateFanModePrevious"

*Edit: You may have to add some 1 second delays (delay 1) to ensure the signal is received by the thermostat. The Venstar adapter is wireless and therefore more prone to getting error messages like: “send failed (no acknowledgment)”.