Creating a Simple app with CherryPy and React JS

Hi folks,

In this tutorial we will set up a simple web app using CherryPy and React js. This tutorial is based on the first tutorial on the CherryPy website with some minor updates necessary to make it work.

First, create a virtual environment, say cherrypyapp using the command:

mkvirtualenv cherrypyapp

The above command switches you to the environment on creation.
Next, install CherryPy using the following commands:

hg clone https://bitbucket.org/cherrypy/cherrypy
cd cherrypy
python setup.py install

Note: As of the time of writing the pip version didn’t seem to work well. On testing the installation there was an error about tutorial.conf not being found, hence my using hg.

Test your installation using command:

python -m cherrypy.tutorial.tut01_helloworld

You should see several lines of text show up with the last line containing the text:

ENGINE Bus STARTED

Now we’re ready to create a project. Create a folder for the project, say ~/Documents/cherrypyapp and change to the directory.

Just like in the tutorial on CherryPy website, we’re going to create a sample app that generates a random string of a user-supplied length (8 by default). The app will allow you edit or delete the string. SQLite db will be used to store the string generated.

Create a file cherrypyapp.py containing the following content:

import os, os.path
import random
import sqlite3
import string
import time

import cherrypy

DB_STRING = "my.db"

class StringGenerator(object):
   @cherrypy.expose
   def index(self):
       return file('index.html')


class StringGeneratorWebService(object):
    exposed = True

    @cherrypy.tools.accept(media='text/plain')
    def GET(self):
        with sqlite3.connect(DB_STRING) as c:
            cherrypy.session['ts'] = time.time()
            r = c.execute("SELECT value FROM user_string WHERE session_id=?",
                      [cherrypy.session.id])
            return r.fetchone()

    def POST(self, length=8):
        some_string = ''.join(random.sample(string.hexdigits, int(length)))
        with sqlite3.connect(DB_STRING) as c:
            cherrypy.session['ts'] = time.time()
            c.execute("INSERT INTO user_string VALUES (?, ?)",
                      [cherrypy.session.id, some_string])
        return some_string

    def PUT(self, another_string):
        with sqlite3.connect(DB_STRING) as c:
            cherrypy.session['ts'] = time.time()
            c.execute("UPDATE user_string SET value=? WHERE session_id=?",
                      [another_string, cherrypy.session.id])

    def DELETE(self):
        cherrypy.session.pop('ts', None)
        with sqlite3.connect(DB_STRING) as c:
            c.execute("DELETE FROM user_string WHERE session_id=?",
                      [cherrypy.session.id])

def setup_database():
    """
    Create the `user_string` table in the database
    on server startup
    """
    with sqlite3.connect(DB_STRING) as con:
        con.execute("CREATE TABLE user_string (session_id, value)")

def cleanup_database():
    """
    Destroy the `user_string` table from the database
    on server shutdown.
    """
    with sqlite3.connect(DB_STRING) as con:
        con.execute("DROP TABLE user_string")

if __name__ == '__main__':
    conf = {
        '/': {
            'tools.sessions.on': True,
            'tools.staticdir.root': os.path.abspath(os.getcwd())
        },
        '/generator': {
            'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
            'tools.response_headers.on': True,
            'tools.response_headers.headers': [('Content-Type', 'text/plain')],
        },
        '/static': {
            'tools.staticdir.on': True,
            'tools.staticdir.dir': './public'
        }
    }

    cherrypy.engine.subscribe('start', setup_database)
    cherrypy.engine.subscribe('stop', cleanup_database)

    webapp = StringGenerator()
    webapp.generator = StringGeneratorWebService()
    cherrypy.quickstart(webapp, '/', conf)

The StringGenerator class contains an index() function which handles the home page link. As seen, it just outputs the contents of index.html.

StringGeneratorWebService is a class that provides a RESTful API for creating, updating and deleting a string which we generate in the front end. The setup_database() function creates the user_string table and the cleanup_database() function drops it.

In the conf variable the line ‘request.dispatch’: cherrypy.dispatch.MethodDispatcher() indicates that we will use HTTP request methods that match the name of functions within the StringGeneratorWebService class.

Next, create a file index.html with the following contetent:

<!DOCTYPE html>
<html>
 <head>
 <link href="/static/css/style.css" rel="stylesheet">

 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js"></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.js"></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

 </head>
 <body>
 <div id="generator"></div>
 <script type="text/babel" src="static/js/gen.js"></script>
 </body>
</html>

Note the script type “text/babel”. That’s important. Don’t use just javascript, or else it may not work.

Next, create a folder public/css. Then create a file style.css inside it with the following content:

body {
  background-color: blue;
}

#the-string {
  display: none;
}

Then create a folder public/js and a file gen.js inside it with the following content:

 var StringGeneratorBox = React.createClass({
   handleGenerate: function() {
     var length = this.state.length;
     this.setState(function() {
       $.ajax({
         url: this.props.url,
         dataType: 'text',
         type: 'POST',
         data: {
           "length": length
         },
         success: function(data) {
           this.setState({
             length: length,
             string: data,
             mode: "edit"
           });
         }.bind(this),
         error: function(xhr, status, err) {
           console.error(this.props.url,
             status, err.toString()
           );
         }.bind(this)
       });
     });
   },
   handleEdit: function() {
     var new_string = this.state.string;
     this.setState(function() {
       $.ajax({
         url: this.props.url,
         type: 'PUT',
         data: {
           "another_string": new_string
         },
         success: function() {
           this.setState({
             length: new_string.length,
             string: new_string,
             mode: "edit"
           });
         }.bind(this),
         error: function(xhr, status, err) {
           console.error(this.props.url,
             status, err.toString()
           );
         }.bind(this)
       });
     });
   },
   handleDelete: function() {
     this.setState(function() {
       $.ajax({
         url: this.props.url,
         type: 'DELETE',
         success: function() {
           this.setState({
             length: "8",
             string: "",
             mode: "create"
           });
         }.bind(this),
         error: function(xhr, status, err) {
           console.error(this.props.url,
             status, err.toString()
           );
         }.bind(this)
       });
     });
   },
   handleLengthChange: function(length) {
     this.setState({
       length: length,
       string: "",
       mode: "create"
     });
   },
   handleStringChange: function(new_string) {
     this.setState({
       length: new_string.length,
       string: new_string,
       mode: "edit"
     });
   },
   getInitialState: function() {
     return {
       length: "8",
       string: "",
       mode: "create"
     };
   },
  render: function() {
     return (
       <div className="stringGenBox">
     <StringGeneratorForm onCreateString={this.handleGenerate}
     onReplaceString={this.handleEdit}
     onDeleteString={this.handleDelete}
     onLengthChange={this.handleLengthChange}
     onStringChange={this.handleStringChange}
     mode={this.state.mode}
     length={this.state.length}
     string={this.state.string}/>        
       </div>
     );
   }
 });

 var StringGeneratorForm = React.createClass({
   handleCreate: function(e) {
     e.preventDefault();
     this.props.onCreateString();
   },
   handleReplace: function(e) {
     e.preventDefault();
     this.props.onReplaceString();
   },
   handleDelete: function(e) {
     e.preventDefault();
     this.props.onDeleteString();
   },
   handleLengthChange: function(e) {
     e.preventDefault();
     var length = React.findDOMNode(this.refs.length).value.trim();
     this.props.onLengthChange(length);
   },
   handleStringChange: function(e) {
     e.preventDefault();
     var string = React.findDOMNode(this.refs.string).value.trim();
     this.props.onStringChange(string);
   },
   render: function() {
     if (this.props.mode == "create") {
       return (
         <div>
            <input type="text" value="{this.props.length}" />
            <button>Give it now!</button>
         </div>
       );
     } else if (this.props.mode == "edit") {
       return (
         <div>
            <input type="text" value="{this.props.string}" />
            <button>Replace</button>
            <button>Delete it</button>
         </div>
       );
     }

     return null;
   }
 });

 React.render(
   <StringGeneratorBox url="/generator" />,
   document.getElementById('generator')
 );

Note from the javascript file that the StringGeneratorBox displays the main container and inside it is the StringGeneratorForm which handles showing the form.

Now we have all the files in place, start the server using the command:

python cherrypyapp.py

You should then be able to visit the site in a browser at http://localhost:8080/

That’s all for now. Enjoy.

Sources

Tutorial | React. https://facebook.github.io/react/docs/tutorial.html

Tutorials — CherryPy 3.8.2 documentation. http://docs.cherrypy.org/en/latest/tutorials.html