Writing your first RapidSMS app

One of the best ways to learn anything new is by learning by example.

In this tutorial, we’ll be building a RapidSMS app to track vehicle mileage.

First, let’s consider the problem: We are interested in building an application capable of allowing a motorist to track his vehicle mileage.

The idea is that before starting a trip, the driver sends the mileage reading on his odometer to RapidSMS and on completing the trip, sends the mileage reading on the odometer again to RapidSMS.

RapidSMS in responding to the second message will report cool things like the total distance travelled and the average speed.

Here are certain things we will be keeping track of:

  1. The time the messages were sent
  2. The mileage readings
  3. The driver (we would need some way to identify the driver)

Installing RapidSMS

The best way to install RapidSMS is to clone it from its repository (this way you are sure to be installing the bleeding edge version of the software). In order to do this, you’ll need to have Git installed on your machine. Please visit http://git-scm.com/ for more information on getting Git installed.

Assuming you’ve installed git, the following steps will get RapidSMS setup on your machine:

~$ git clone git://github.com/rapidsms/rapidsms.git
~$ cd rapidsms
~rapidsms$

Configuring RapidSMS

RapidSMS configuration options are stored in the rapidsms.ini file in the root directory of the application. You can modify the settings here using your favorite text editor. If you’ll be doing development work however, we recommend copying the file to a new file called local.ini.

During the initialization of RapidSMS, the application will look in the directory for any file named local.ini for configuration settings. If this is not found, then rapidsms.ini will be loaded instead.

In the configuration file, there are several configuration options and sections for now, we’ll not treat them.

Firing up RapidSMS

Before starting RapidSMS, we need to first of all synchronize the database. This step allows all necessary database tables which are persistent stores for the Django database models that RapidSMS uses.:

~rapidsms$ python rapidsms syncdb
Creating table auth_permission
Creating table auth_group
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
Creating table django_admin_log
Creating table httptester_message
Creating table locations_locationtype
Creating table locations_location
Creating table patterns_pattern
Creating table reporters_role
Creating table reporters_reportergroup
Creating table reporters_reporter
Creating table reporters_persistantbackend
Creating table reporters_persistantconnection
Creating table logger_incomingmessage
Creating table logger_outgoingmessage

You just installed Django's auth system, which means you don't have any superusers defined.
Would you like to create one now? (yes/no):

Your output may vary depending on the type of applications that are installed and/or enabled. You can answer yes to the question to create a superuser and fill out whatever you want to use as username, email address and password.:

Would you like to create one now? (yes/no): yes
Username (Leave blank to use 'username'):
E-mail address: username@domain.com
Password:
Password (again):
Superuser created successfully.
Installing index for auth.Permission model
Installing index for auth.Message model
Installing index for admin.LogEntry model
Installing index for locations.Location model
Installing index for reporters.ReporterGroup model
Installing index for reporters.Reporter model
Installing index for reporters.PersistantConnection model
~rapidsms$

Now that our database has been synchronized, we can fireup the internal webserver:

~rapidsms$ python rapidsms runserver 8000

Now try opening the following url in your web browser: http://localhost:8000/

_images/rapidsms-start.png

Congratulations you have successfully installed RapidSMS and gotten it to run.

Creating the application stub

RapidSMS comes with tools to make creating applications as easy as possible and one of such is to create the skeleton for your application so you can just write the logic where you need the processing to be done.

To create the application skeleton type the following:

~rapidsms$ python rapidsms startapp mileage
Don't forget to add 'mileage' to your rapidsms.ini apps.
~rapidsms$

As you can see in the message, in order for the application to be of any use it has to be inserted in the list of applications. Applications are not enabled in RapidSMS unless they are included in the list of apps.

Make a copy of the rapidsms configuration file rapidsms.ini:

~rapidsms$ cp rapidsms.ini local.ini

Next, use your favorite text editor and open the local.ini file. Under the [rapidsms] section, add mileage at the end of apps directive so it looks like this:

[rapidsms]
apps=webui,ajax,admin,patterns,reporters,locations,messaging,httptester,logger,echo,mileage
backends=http

This step has created a directory named mileage in your RapidSMS apps directory which is located at rapidsms/apps. We will change directories into this so we can work with it.:

~rapidsms$ cd apps/mileage
~rapidsms/apps/mileage$

In this directory, are four files of importance:

  • app.py

    This file is where your application logic is stored. We’ll be working more with this file.

  • models.py

    Your Django models are defined in this file. We’ll be using this file to define the model structure for persisting data in the database

  • tests.py

    One of the ideologies used in developing RapidSMS is the test-driven design ideology. You store unit tests for your app in this file.

  • views.py

    Django is at the core of RapidSMS and for apps that require a web interface, the views logic is defined here.

Defining the Django model

Open the models.py file in your favorite text editor and type in or copy and paste the following code listing in it.:

from django.db import models

# Create your Django models here, if you need them.
class Mileage(models.Model):
    start_mileage = models.PositiveIntegerField(null=False, default=0)
    start_time = models.DateTimeField(null=True)
    stop_mileage = models.PositiveIntegerField(null=False, default=0)
    stop_time = models.DateTimeField(null=True)
    reporter = models.CharField(null=False, max_length=20)
    completed = models.BooleanField(default=False)

Syncing the database

After defining your Django model, you need to synchronize these changes. Synchronizing these changes will create the necessary database tables and indexes that will be used for storing and retrieving data.

Type in the following from your project directory:

~rapidsms$ python rapidsms syncdb
Creating table mileage_mileage
~rapidsms$

Writing the application logic

Ok now things really get interesting. We are going to write application logic that will take an incoming message, parse it, extract data, store in in a Django model (which maps to a database table) and send a response.

So you can type in the code and reflect on how it works later, I’m providing the code listing below. This should go into the app.py file in the mileage app folder.:

# vim: ai sts=4 sw=4 ts=4 et
import rapidsms
import re
from models import *
from datetime import datetime

class App (rapidsms.app.App):

    # message pattern expected: 'mileage start number' or 'mileage stop number'
    # in order to take care of possible typos it will accept a mispelled milage
    # for the keyword: 'milage start number' or 'milage stop number'
    pattern = re.compile(r'^mile?age\s+(start|stop)\s+(\d+)', re.IGNORECASE)

    def start (self):
        """Configure your app in the start phase."""
        pass

    def parse (self, message):
        """Parse and annotate messages in the parse phase."""
        pass

    def handle (self, message):
        response = self.pattern.findall(message.text)
        if response:
            entry = response[0]
            entry_time = datetime.now()
            reporter = message.connection.identity
            mileage = int(entry[1])

            if entry[0].lower() == "start":
                # Persist entry in the database
                Mileage(
                    start_mileage=mileage,
                    start_time=entry_time,
                    reporter=reporter).save()

                # Generate a response
                message.respond("After your trip is completed, please send: MILEAGE STOP mileage_reading")
            elif entry[0].lower() == "stop":
                try:
                    # We attempt to find the latest "open" entry. An "open"
                    # entry is one that has been created using MILEAGE START
                    # but has not been "closed" with a MILEAGE STOP
                    reading = Mileage.objects.filter(completed=False,reporter=reporter).order_by('-start_time')[0]
                    if reading:
                        reading.stop_mileage = mileage
                        reading.stop_time = entry_time
                        reading.completed = True
                        reading.save()

                        # Reporting...
                        distance = reading.stop_mileage - reading.start_mileage
                        distance_string = "%d miles" % distance if distance > 1 else "%d mile" % distance
                        total_time = reading.stop_time - reading.start_time
                        # Had to do the following to prevent integer division by zero
                        total_seconds = 1 if not total_time.seconds else total_time.seconds
                        speed = (float(distance) / total_seconds) * 3600

                        days = hours = minutes = 0
                        time_string = ""

                        # While generating the time string, humanize the output a little bit
                        days = total_time.days
                        if days:
                            time_string += "%d days " % days if days > 1 else "%d day " % days
                        hours = total_time.seconds / 3600
                        if hours:
                            time_string += "%d hrs " % hours if hours > 1 else "%d hr " % hours
                        minutes = (total_time.seconds % 3600) / 60
                        if minutes:
                            time_string += "%d mins " % minutes if minutes > 1 else "%d min " % minutes
                        seconds = total_time.seconds % 60
                        time_string += "%d secs" % seconds if seconds > 1 else "%d sec" % seconds

                        # Generate response to send back
                        response = "Your trip of %s took %s. Your average speed was %dmph" % (distance_string, time_string.strip(), speed)
                        message.respond(response)
                except (Mileage.DoesNotExist, IndexError):
                    message.respond("You cannot complete a trip you didn't start :)")

            return True
        else:
            return False

    def cleanup (self, message):
        """Perform any clean up after all handlers have run in the
           cleanup phase."""
        pass

    def outgoing (self, message):
        """Handle outgoing message notifications."""
        pass

    def stop (self):
        """Perform global app cleanup when the application is stopped."""
        pass

In the code listing above, we create our app by creating a class named App that inherits from rapidsms.app.App. This is the convention. Thankfully, the startapp command creates this so all you need to do is fill in your code. As you would have noticed, we are only writing code to override the handle method.

Our application gets the contents of the text message from the message parameter that is passed to the method as a parameter. The message parameter is an object from where we can read the contents of the message, respond to the message or even read the sender of the message.

Here are a couple of useful attributes and methods available in the message object:

  • message.text

    This attribute contains the message contents

  • message.respond(response)

    Use this method to respond to a message. response is a string with the contents of the response.

  • message.date

    In cases where you need to access the time the message sent, this is how you gain access to this value. It’s a datetime object so you might need the datetime python library to manipulate it.

  • message.connection

    This is an object that allows you to access connection parameters like the identity (phone number, irc nickname, etc.) and backend. Most times you will need only the identity and you can access it from the attribute message.connection.identity.

You’ll also notice the use of an object named Mileage. This is the Django model we defined earlier for storing data.

So we receive a message containing the string mileage start 2000920. This message gets processed by the handle method:

  1. We do a regex search on the string. If it matches the pattern, it is processed if not, it is left alone. During processing, we read the sender of the message, the date and time the message was sent and the mileage reading.

  2. We check if the message is a start message or stop message.

  3. If it is a start message, we store those parameters we’ve extracted from the message in the database.

  4. If it is a stop message, we attempt to retrieve the latest uncompleted entry from the database. If this doesn’t exist, we assume that the reporter is attempting to stop a trip that has not been started and we send a friendly response back. If we find an entry, we update the entry with the stop mileage, stop time and set the entry as completed.

    We then do a few calculations to determine the trip time, distance and average speed. We then generate a response and send this to reporter.

Note

The ordering of your apps in the configuration file is quite important. Every app in the apps directive gets to process messages in the order in which they are listed. The echo app for instance, simply echos whatever it receives so you would want the mileage app or your own app to come before it or you can totally disable it by removing it from the list.

Testing your application

Testing your app, can be done by using a simple backend included in RapidSMS called the http backend. By default, this backend is enabled.

To access this backend, you visit http://localhost:8000/http

For this to work, your RapidSMS router and included RapidSMS internal webserver must be running.

Starting the RapidSMS router

This is how to start the RapidSMS router:

~rapidsms$ python rapidsms router

You will see a couple of startup messages. If all goes well, you should see the following at the end.:

2009-07-09 17:28:36,637 INFO [router]: SERVING FOREVER...

To stop the router, press CONTROL-C

Starting the RapidSMS internal webserver

Earlier in this tutorial, we started the internal webserver. The procedure remains the same. This however, should be done in a separate window:

~rapidsms$ python rapidsms runserver 8000
Validating models...
0 errors found

Django version 1.0.2 final, using settings 'webui.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Now you can visit http://localhost:8000/http on your web browser.

Type in any phone number you want to use for the test and type in your message: e.g. mileage start 2000. A couple of moments later (just to simulate a delay), send another message with the content: mileage stop 2020 (for instance).

You will see output similar to the screenshot below:

_images/rapidsms-mileage-test.png

The red lines indicate messages you send to RapidSMS and the green lines indicate messages you receive from RapidSMS.

Congratulations! You’ve just written and tested your first RapidSMS application.