Choose Your Own Adventure Presentations with Reveal.js, Python and WebSockets

November 06, 2014
Written by
Matt Makai
Twilion

choose-your-own-adventure

You’re preparing a technical talk on your new favorite open source project to present to your local software meetup group.

—————

How do you proceed? If you choose to create another passe linear slide deck, load up Microsoft PowerPoint. If you decide to build a childhood nostalgia-packed Choose Your Own Adventure presentation, continue reading this blog post.

—————

Good choice! To create our Choose Your Own Adventure presentation we’ll use Reveal.js, Flask, WebSockets and Twilio SMS. Our final product will have decision screens like the following screenshot where SMS votes from the audience are counted and displayed in real-time to determine the next step in the presentation story. Once the votes are tallied the presenter can click the left choice or the right choice to go to the appropriate next slide in the presentation.

Take a look at the DjangoCon US 2014 ”Choose Your Own Django Deployment Adventure” video if you want to see an example of a Choose Your Own Adventure presentation in action.

What we’ll need

  • Reveal.js for browser-based presentations
  • Python with application dependencies:
  • Redis for calculating results and short-term vote storage
  • A Twilio account with an SMS number so audiences can vote which path the presentation should follow
  • Ngrok for a secure tunnel to our local server running the presentation

Do you want to skip typing out the code yourself? Check out this open source repository on GitHub. The Git repository contains the final code as well as intermediate tags named tutorial-step-1, tutorial-step-2 and tutorial-step-3 for each section below.

Building the Presentation

Let’s first create the directory structure necessary for our project. The nested subdirectories will look like the following screenshot. Our base directory name and Flask app directory name will be “cyoa”, an acronym for “Choose Your Own Adventure.”

At the base cyoa/ directory create a file named requirements.txt with the following content in it. These are the dependencies for our project that we’ll install in a moment.

Flask==0.10.1
Flask-Script==2.0.5
Flask-SocketIO==0.4.1
gunicorn==19.1.1

redis==2.10.3

twilio==6.5.0

This application uses Flask to serve up the presentation. The Flask-Script extension will assist us in creating a manage.py file with helper commands to run the web application. The Flask-SocketIO extension handles the server-side websockets communication.

First create a new virtualenv outside the base project directory to separate your Python dependencies from other apps you’re working on.

virtualenv cyoa

Activate the virtualenv before we install the dependencies.

source cyoa/bin/activate

To install these dependencies run the following pip command on the command line.

pip install -r requirements.txt

Create a file named cyoa/manage.py with the following contents.

from gevent import monkey
monkey.patch_all()

import os

from cyoa import app, redis_db, socketio
from flask.ext.script import Manager, Shell

manager = Manager(app)

def make_shell_context():
    return dict(app=app, redis_db=redis_db)

manager.add_command("shell", Shell(make_context=make_shell_context))

@manager.command
def runserver():
    socketio.run(app, "0.0.0.0", port=5001)

if __name__ == '__main__':
    manager.run()

The above manage.py file will help us run our Flask application with the python manage.py runserver command once we’ve put more of the pieces in places.

Next create a file cyoa/cyoa/config.py (the cyoa/cyoa subdirectory is where most of our Python code will live other than manage.py) and add the below code to create the configuration variables we’ll need for our Flask application.

import os

# General Flask app settings
DEBUG = os.environ.get('DEBUG', None)
SECRET_KEY = os.environ.get('SECRET_KEY', None)

# Redis connection
REDIS_SERVER = os.environ.get('REDIS_SERVER', None)
REDIS_PORT = os.environ.get('REDIS_PORT', None)
REDIS_DB = os.environ.get('REDIS_DB', None)

# Twilio API credentials
TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID', None)
TWILIO_AUTH_TOKEN = os.environ.get('TWILIO_AUTH_TOKEN', None)
TWILIO_NUMBER = os.environ.get('TWILIO_NUMBER', None)

Here’s a run down of what each of these environment variables specified in config.py is for:

  1. DEBUG – True or False for whether Flask should display error messages if something goes wrong
  2. SECRET_KEY – a long key that should be kept secret
  3. REDIS_SERVER – in this case likely to be localhost or wherever Redis is running
  4. REDIS_PORT – generally set to 6379 for the default Redis port
  5. REDIS_DB – set to 0
  6. TWILIO_ACCOUNT_SID – found on your Twilio account dashboard
  7. TWILIO_AUTH_TOKEN – also found on your Twilio account dashboard
  8. TWILIO_NUMBER – a number you’ve purchased on Twilio

Set the DEBUG and SECRET_KEY environment variables now. The Redis and Twilio environment variables will be set in the next section.

Setting up environment variables depends on your operating system. There are guides for every major operating system, whether that is Ubuntu Linux, Mac OS X and Windows.

The next file we need to create is cyoa/cyoa/__init__.py, which will set up the core pieces for our the Flask app.

from flask import Flask
from flask.ext.socketio import SocketIO
import redis

app = Flask(__name__, static_url_path='/static')
app.config.from_pyfile('config.py')

from config import REDIS_SERVER, REDIS_PORT, REDIS_DB

redis_db = redis.StrictRedis(host=REDIS_SERVER, port=REDIS_PORT, db=REDIS_DB)

socketio = SocketIO(app)

from . import views

Next create a file named cyoa/cyoa/views.py and put the following code inside.

from flask import render_template, abort
from jinja2 import TemplateNotFound

from . import app

@app.route('/<presentation_name>/', methods=['GET'])
def landing(presentation_name):
    try:
        return render_template(presentation_name + '.html')
    except TemplateNotFound:
        abort(404)

For now the above file contains a single view named landing. This view landing obtains a presentation name from the URL and checks the cyoa/cyoa/templates/ directory for an HTML template file matching that name. If a matching file is found, landing renders the HTML template. If no file name matches the presentation name in the URL, landing returns a 404 HTTP response.

We’re awfully close to getting to fire up this presentation to take a look at it. An HTML template file along with some static files are necessary for displaying the presentation. Create a Reveal.js presentation in templates directory named cyoa/cyoa/templates/cyoa.html. Add the following HTML inside this file.

<!doctype html>
<html lang="en">
 <head>
 <meta charset="utf-8">
 <title>Choose Your Own Adventure Presentations with Reveal.js!</title>
 <meta name="apple-mobile-web-app-capable" content="yes" />
 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui">
 <link rel="stylesheet" href="/static/css/reveal.css">
 <link rel="stylesheet" href="/static/css/default.css" id="theme">
 <link rel="stylesheet" href="/static/css/zenburn.css">
 <!--[if lt IE 9]>
 <script src="lib/js/html5shiv.js"></script>
 <![endif]-->
 </head>
 <body>
 <div class="reveal">
 <div class="slides">
 <section>
 <h1>Choose Your Own Adventure!</h1>
 <h3>With SMS voting</h3>
 </section>

 <section>
 <div>
 <h2>Text "left" or "right" to</h2>
 <!-- replace this with your Twilio number -->
 <h1>(xxx) 555-1234</h1>
 </div>
 <br>
 <div style="display: inline;">
 <div style="float: left;">
 <h2><a href="#/2">left</a></h2>
 <h1><div id="left">0</div> votes</h1>
 </div>
 <div style="float: right;">
 <h2><a href="#/3">right</a></h2>
 <h1><div id="right">0</div> votes</h1>
 </div>
 </div>
 </section>

 <!-- linked from first choice -->
 <section>
 <h1>Left</h1>
 </section>

 <!-- linked from second choice -->
 <section>
 <h1>Right</h1>
 </section>

 </div>
 </div>

 <script type="text/javascript"
 src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
 <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/0.9.16/socket.io.min.js"></script>
 <script src="/static/js/reveal.js"></script>

 <script>
 Reveal.initialize({
 controls: true,
 progress: true,
 history: true,
 center: true,
 theme: Reveal.getQueryHash().theme,
 transition: Reveal.getQueryHash().transition || 'default',
 dependencies: []
 });
 </script>
 </body>
</html>   

Choose Your Own Adventure!

 

With SMS voting

 

 

 

left

 

0

votes

 

 

right

 

votes

 

 

 

Left

 

Right

 

 

 

As you can see in the HTML there are three locally hosted static CSS files and a JavaScript file you’ll need for your project. Download these files from GitHub or download and extract this cyoa.tar.gz archive into the cyoa/cyoa/static directory. The CSS and JavaScript files we need are the following:

  • js/reveal.js – code for creating the beautiful Reveal.js presentation
  • css/default.css – default style for Reveal.js decks
  • css/reveal.css – required for running Reveal.js presentations properly
  • css/zenburn.css – necessary for syntax highlighting within a presentation

With these files in place we can try out the presentation to make sure everything so far is in working order. Fire up Flask with the following command.

python manage.py runserver

Now we can view the initial version of the presentation by going to http://localhost:5001/cyoa/ in the browser. Flask will find the cyoa.html file in the templates directly and serve that up so you can view it.

However, the presentation will only come up on your own computer. To access the presentation from other computers you’ll need to deploy to a hosting server or create a localhost tunnel using a tool such as Ngrok. Sign up for Ngrok and download the Ngrok tunnel application.

Fire up ngrok on port 5001 where our Flask application is running. See this configuring Ngrok post if you’re running Windows. On Linux and Mac OS X Ngrok can be run with the following command when you’re in the directory that ngrok is located in.

ngrok http 5001

You’ll see a screen like the following. Take note of the unique https:// forwarding URL as we’ll need that again in a minute for the Twilio webhook.

You can now pull the presentation both from the localhost URL as well as the forwarding URL set up by ngrok.

Accepting SMS votes

So far we’ve written the Python code to serve a Reveal.js presentation and exposed it via a localhost tunnel. Now we need a way for the audience to vote on which path they want the presentation to go. To do that we’ll show a Twilio phone number on the screen and use Twilio SMS to let the audience vote. If you’ve already got an account grab a new or existing phone number, otherwise sign up and upgrade a Twilio account.

We’ll need to set up the message webhook so that each vote SMS to the Twilio number is sent to our Flask app. The Twilio number screen should look like the following.

twilio-webhook.png

Paste in your Ngrok forwarding URL, which will look like “https://[unique Ngrok code].ngrok.com”, along with “/cyoa/twilio/webhook/” to the Messaging Request URL text box then hit save. For example, if your Ngrok tunnel is “1d4e144c” your URL to paste into the webhook text box should look like the following URL.

 

https://1d4e144c.ngrok.com/cyoa/twilio/webhook/

 

Now Twilio will send a POST HTTP request to the ngrok port forwarding URL which will be sent down to your localhost server.

Make sure to update the phone number in your cyoa/cyoa/templates/cyoa.html presentation file. Look for the line like the following and replace it with your Twilio number.

<!-- update your number here -->
(xxx) 555-1234

However, we’re not yet ready to accept those incoming SMS votes. First we need to ensure Redis is running and write Python code to persist votes to it.

Ensure that Redis server is installed on your system since that will be our temporary vote storage system. There’s a quickstart installation guide available for your operating system of choice if you’re unfamiliar with Redis. In that quickstart you just need to go from the beginning through the “Starting Redis” section and you’ll be set for finishing this project.

Let’s add code in our cyoa/cyoa/views.py file for the Twilio webhook HTTP POST that occurs on an inbound SMS. We’ll increment a counter for each keyword texted in by people in the audience.


import cgi
from flask import render_template, abort, request
from jinja2 import TemplateNotFound
from twilio.twiml.messaging_response import MessagingResponse
from twilio.rest import Client

from .config import TWILIO_NUMBER
from . import app, redis_db

client = Client()

@app.route('/<presentation_name>/', methods=['GET'])
def landing(presentation_name):
    try:
        return render_template(presentation_name + '.html')
    except TemplateNotFound:
        abort(404)

@app.route('/cyoa/twilio/webhook/', methods=['POST'])
def twilio_callback():
    to = request.form.get('To', '')
    from_ = request.form.get('From', '')
    message = request.form.get('Body', '').lower()
    if to == TWILIO_NUMBER:
        redis_db.incr(cgi.escape(message))
    resp = MessagingResponse()
    resp.message("Thanks for your vote!")
    return str(resp)

It’d be useful to have a way to clear out votes from Redis in between rehearsals for our presentation. Update cyoa/manage.py with a new function to clear votes from Redis.


from gevent import monkey
monkey.patch_all()

import os
import redis

from cyoa import app, redis_db, socketio
from flask.ext.script import Manager, Shell

manager = Manager(app)

def make_shell_context():
    return dict(app=app, redis_db=redis_db)

manager.add_command("shell", Shell(make_context=make_shell_context))

@manager.command
def runserver():
    socketio.run(app, "0.0.0.0", port=5001)

@manager.command
def clear_redis():
    redis_cli = redis.StrictRedis(host='localhost', port='6379', db='0')
    redis_cli.delete('left')
    redis_cli.delete('right')

if __name__ == '__main__':
    manager.run()

If Flask and ngrok are still running we can test this out now, otherwise fire them up and let’s text either “left” or “right” to your Twilio number.

When you test out SMS inbound text messages you should receive the “Thanks for your vote!” response like in the screenshot below.

At this point those inbound text message votes are also being stored in Redis. We’re so close to wrapping up this code at this point let’s write a bit more Python to test that the messages are indeed in Redis and propagate the results to the presentation via websockets.

Adding WebSockets for Real-time Updates

We need one more new Python file to handle the server-side websockets communication. Websockets allow streams of communication between the client and server. In our presentation that allows the vote counts to be updated on the presentation slide as soon as the server emits a new message. Add a file named cyoa/cyoa/websockets.py with the following code to set up the server-side websockets implementation.

from flask.ext.socketio import emit

from . import socketio

@socketio.on('connect', namespace='/cyoa')
def test_connect():
    pass

@socketio.on('disconnect', namespace='/cyoa')
def test_disconnect():
    pass

The above code in the update_count function allows the websocket clients, in this case the browser that loads the presentation, to connect to the websockets stream and receive future messages.

Add one line to the bottom of cyoa/cyoa/__init__.py to import the websockets.py file we just wrote.


from flask import Flask
from flask.ext.socketio import SocketIO
import redis

app = Flask(__name__, static_url_path='/static')
app.config.from_pyfile('config.py')

from config import REDIS_SERVER, REDIS_PORT, REDIS_DB

redis_db = redis.StrictRedis(host=REDIS_SERVER, port=REDIS_PORT, db=REDIS_DB)

socketio = SocketIO(app)

from . import views
from . import websockets

Update cyoa/cyoa/views.py with the following highlighted lines. We’re adding the socketio.emit call so votes are passed via websockets to the presentation.


import cgi
from flask import render_template, request, abort
from jinja2 import TemplateNotFound
from twilio.twiml.messaging_response import MessagingResponse
from twilio.rest import Client

from .config import TWILIO_NUMBER
from . import app, redis_db
from . import socketio

client = Client()

@app.route('/<presentation_name>/', methods=['GET'])
def landing(presentation_name):
    try:
        return render_template(presentation_name + '.html')
    except TemplateNotFound:
        abort(404)

@app.route('/cyoa/twilio/webhook/', methods=['POST'])
def twilio_callback():
    to = request.form.get('To', '')
    from_ = request.form.get('From', '')
    message = request.form.get('Body', '').lower()
    if to == TWILIO_NUMBER:
        redis_db.incr(cgi.escape(message))
        socketio.emit('msg', {'div': cgi.escape(message),
                              'val': redis_db.get(message)},
                      namespace='/cyoa')

    resp = MessagingResponse()
    resp.message("Thanks for your vote!")
    return str(resp)

Add the highlighted code below to the cyoa/cyoa/templates/cyoa.html HTML presentation file. This code relies on the websockets JavaScript library and JQuery that was included with the file when we originally created it.

<script type="text/javascript">// <![CDATA[ Reveal.initialize({ controls: true, progress: true, history: true, center: true, theme: Reveal.getQueryHash().theme, transition: Reveal.getQueryHash().transition || 'default', dependencies: [ ] }); $(document).ready(function() { namespace = '/cyoa'; var socket = io.connect('http://' + document.domain + ':' + location.port + namespace); /* add and edit choices here */ var appropriate_choices = ['left','right',]; socket.on('msg', function(msg) { /* ensure valid vote and div exists */ if (appropriate_choices.indexOf(msg.div) >= 0) {
                var checkDiv = $('#' + msg.div);
                if (checkDiv.length > 0) {
                    checkDiv.html(msg.val);
                }
            }
        });
    });
// ]]></script>

Fire up Flask with Ngrok again. Check out http://localhost:5001/cyoa/ on your browser and you’ll see the same presentation as before. However, text your vote of ‘left’ or ‘right’ to your number and the votes will appear like in the screenshot below without any page refresh.

Wrapping it up

You now have a Flask app that can serve up Reveal.js presentations where the audience can interact with the slide on screen by texting in their votes.

Now it’s up to you to create the Choose Your Own Adventure content!

Let me know what new Choose Your Own Adventure presentation stories you come up with or open a pull request to improve the code base. Contact me via:

  • Email: makai@twilio.com
  • GitHub: Follow makaimc for repository updates
  • Twitter: @mattmakai