One of the more abstract concepts you'll handle when building your business is what the workflow will look like.
At its core, setting up a standardized workflow is about enabling your service providers (agents, hosts, customer service reps, administrators, and the rest of the gang) to better serve your customers.
To illustrate a very real-world example, today we'll build a Python and Flask webapp for finding and booking vacation properties — tentatively called Airtng.
Here's how it'll work:
We'll be using the Twilio REST API to send our users messages at important junctures. Here's a bit more on our API:
airtng_flask/__init__.py
1import os2from airtng_flask.config import config_env_files3from flask import Flask45from flask_bcrypt import Bcrypt6from flask_sqlalchemy import SQLAlchemy7from flask_login import LoginManager89db = SQLAlchemy()10bcrypt = Bcrypt()11login_manager = LoginManager()121314def create_app(config_name='development', p_db=db, p_bcrypt=bcrypt, p_login_manager=login_manager):15new_app = Flask(__name__)16config_app(config_name, new_app)17new_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False1819p_db.init_app(new_app)20p_bcrypt.init_app(new_app)21p_login_manager.init_app(new_app)22p_login_manager.login_view = 'register'23return new_app242526def config_app(config_name, new_app):27new_app.config.from_object(config_env_files[config_name])282930app = create_app()3132import airtng_flask.views
Let's get started! Click the below button to begin.
For this workflow to work, we need to have Users
created in our application, and allow them to log into Airtng.
Our User
model stores a user's basic information including their phone number. We'll use that to send them SMS notifications later.
airtng_flask/models/user.py
1from airtng_flask.models import app_db2from airtng_flask.models import bcrypt34db = app_db()5bcrypt = bcrypt()678class User(db.Model):9__tablename__ = "users"1011id = db.Column(db.Integer, primary_key=True)12name = db.Column(db.String, nullable=False)13email = db.Column(db.String, nullable=False)14password = db.Column(db.String, nullable=False)15phone_number = db.Column(db.String, nullable=False)1617reservations = db.relationship("Reservation", back_populates="guest")18vacation_properties = db.relationship("VacationProperty", back_populates="host")1920def __init__(self, name, email, password, phone_number):21self.name = name22self.email = email23self.password = bcrypt.generate_password_hash(password)24self.phone_number = phone_number2526def is_authenticated(self):27return True2829def is_active(self):30return True3132def is_anonymous(self):33return False3435def get_id(self):36try:37return unicode(self.id)38except NameError:39return str(self.id)4041# Python 34243def __unicode__(self):44return self.name4546def __repr__(self):47return '<User %r>' % (self.name)
Next, let's look at how we define the VacationProperty model.
In order to build a vacation rentals company we need a way to create the property listings.
The VacationProperty
model belongs to the User
who created it (we'll call this user the host moving forward) and contains only two properties: a description
and an image_url
.
We also include a couple database relationship fields to help us link vacation properties to their hosts as well as to any reservations our users make.
airtng_flask/models/vacation_property.py
1from airtng_flask.models import app_db23db = app_db()456class VacationProperty(db.Model):7__tablename__ = "vacation_properties"89id = db.Column(db.Integer, primary_key=True)10description = db.Column(db.String, nullable=False)11image_url = db.Column(db.String, nullable=False)1213host_id = db.Column(db.Integer, db.ForeignKey('users.id'))14host = db.relationship("User", back_populates="vacation_properties")15reservations = db.relationship("Reservation", back_populates="vacation_property")1617def __init__(self, description, image_url, host):18self.description = description19self.image_url = image_url20self.host = host2122def __repr__(self):23return '<VacationProperty %r %r>' % self.id, self.description
Next we'll take a look at how to model a reservation.
The Reservation
model is at the center of the workflow for this application. It is responsible for keeping track of:
guest
who performed the reservationvacation_property
the guest is requesting (and associated host)status
of the reservation: pending
, confirmed
, or rejected
airtng_flask/models/reservation.py
1from airtng_flask.models import app_db, auth_token, account_sid, phone_number2from flask import render_template3from twilio.rest import Client45db = app_db()678class Reservation(db.Model):9__tablename__ = "reservations"1011id = db.Column(db.Integer, primary_key=True)12message = db.Column(db.String, nullable=False)13status = db.Column(db.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'), default='pending')1415guest_id = db.Column(db.Integer, db.ForeignKey('users.id'))16vacation_property_id = db.Column(db.Integer, db.ForeignKey('vacation_properties.id'))17guest = db.relationship("User", back_populates="reservations")18vacation_property = db.relationship("VacationProperty", back_populates="reservations")1920def __init__(self, message, vacation_property, guest):21self.message = message22self.guest = guest23self.vacation_property = vacation_property24self.status = 'pending'2526def confirm(self):27self.status = 'confirmed'2829def reject(self):30self.status = 'rejected'3132def __repr__(self):33return '<VacationProperty %r %r>' % self.id, self.name3435def notify_host(self):36self._send_message(self.vacation_property.host.phone_number,37render_template('messages/sms_host.txt',38name=self.guest.name,39description=self.vacation_property.description,40message=self.message))4142def notify_guest(self):43self._send_message(self.guest.phone_number,44render_template('messages/sms_guest.txt',45description=self.vacation_property.description,46status=self.status))4748def _get_twilio_client(self):49return Client(account_sid(), auth_token())5051def _send_message(self, to, message):52self._get_twilio_client().messages.create(53to=to,54from_=phone_number(),55body=message)
Now that we have our models, let's see how a user would create a reservation.
The reservation creation form holds only one field, the message that will be sent to the host user when reserving one of her properties.
The rest of the information necessary to create a reservation is taken from the user that is logged into the system and the relationship between a property and its owning host.
A reservation is created with a default status pending
, so when the host replies with an accept
or reject
response, the system knows which reservation
to update.
airtng_flask/views.py
1from airtng_flask import db, bcrypt, app, login_manager2from flask import session, g, request, flash, Blueprint3from flask_login import login_user, logout_user, current_user, login_required4from twilio.twiml.voice_response import VoiceResponse56from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \7ReservationConfirmationForm8from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params9from airtng_flask.models import init_models_module1011init_models_module(db, bcrypt, app)1213from airtng_flask.models.user import User14from airtng_flask.models.vacation_property import VacationProperty15from airtng_flask.models.reservation import Reservation161718@app.route('/', methods=["GET", "POST"])19@app.route('/register', methods=["GET", "POST"])20def register():21form = RegisterForm()22if request.method == 'POST':23if form.validate_on_submit():2425if User.query.filter(User.email == form.email.data).count() > 0:26form.email.errors.append("Email address already in use.")27return view('register', form)2829user = User(30name=form.name.data,31email=form.email.data,32password=form.password.data,33phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data)34)35db.session.add(user)36db.session.commit()37login_user(user, remember=True)3839return redirect_to('home')40else:41return view('register', form)4243return view('register', form)444546@app.route('/login', methods=["GET", "POST"])47def login():48form = LoginForm()49if request.method == 'POST':50if form.validate_on_submit():51candidate_user = User.query.filter(User.email == form.email.data).first()5253if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,54form.password.data):55form.password.errors.append("Invalid credentials.")56return view('login', form)5758login_user(candidate_user, remember=True)59return redirect_to('home')60return view('login', form)616263@app.route('/logout', methods=["POST"])64@login_required65def logout():66logout_user()67return redirect_to('home')686970@app.route('/home', methods=["GET"])71@login_required72def home():73return view('home')747576@app.route('/properties', methods=["GET"])77@login_required78def properties():79vacation_properties = VacationProperty.query.all()80return view_with_params('properties', vacation_properties=vacation_properties)818283@app.route('/properties/new', methods=["GET", "POST"])84@login_required85def new_property():86form = VacationPropertyForm()87if request.method == 'POST':88if form.validate_on_submit():89host = User.query.get(current_user.get_id())9091property = VacationProperty(form.description.data, form.image_url.data, host)92db.session.add(property)93db.session.commit()94return redirect_to('properties')9596return view('property_new', form)979899@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})100@app.route('/reservations/<property_id>', methods=["GET", "POST"])101@login_required102def new_reservation(property_id):103vacation_property = None104form = ReservationForm()105form.property_id.data = property_id106107if request.method == 'POST':108if form.validate_on_submit():109guest = User.query.get(current_user.get_id())110111vacation_property = VacationProperty.query.get(form.property_id.data)112reservation = Reservation(form.message.data, vacation_property, guest)113db.session.add(reservation)114db.session.commit()115116reservation.notify_host()117118return redirect_to('properties')119120if property_id is not None:121vacation_property = VacationProperty.query.get(property_id)122123return view_with_params('reservation', vacation_property=vacation_property, form=form)124125126@app.route('/confirm', methods=["POST"])127def confirm_reservation():128form = ReservationConfirmationForm()129sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."130131user = User.query.filter(User.phone_number == form.From.data).first()132reservation = Reservation \133.query \134.filter(Reservation.status == 'pending'135and Reservation.vacation_property.host.id == user.id) \136.first()137138if reservation is not None:139140if 'yes' in form.Body.data or 'accept' in form.Body.data:141reservation.confirm()142else:143reservation.reject()144145db.session.commit()146147sms_response_text = "You have successfully {0} the reservation".format(reservation.status)148reservation.notify_guest()149150return twiml(_respond_message(sms_response_text))151152153# controller utils154@app.before_request155def before_request():156g.user = current_user157uri_pattern = request.url_rule158if current_user.is_authenticated and (159uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):160redirect_to('home')161162163@login_manager.user_loader164def load_user(id):165try:166return User.query.get(id)167except:168return None169170171def _respond_message(message):172response = VoiceResponse()173response.message(message)174return response
Now that we have seen how we will initiate a reservation, let's look at how to notify the host.
When a reservation is created for a property, we want to notify the host
of the reservation request.
We use Twilio's Rest API to send a SMS message to the host, using a Twilio phone number.
Now we just have to wait for the host to send an SMS response accepting or rejecting the reservation. At that point we can notify the user and host and update the reservation information accordingly.
airtng_flask/models/reservation.py
1from airtng_flask.models import app_db, auth_token, account_sid, phone_number2from flask import render_template3from twilio.rest import Client45db = app_db()678class Reservation(db.Model):9__tablename__ = "reservations"1011id = db.Column(db.Integer, primary_key=True)12message = db.Column(db.String, nullable=False)13status = db.Column(db.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'), default='pending')1415guest_id = db.Column(db.Integer, db.ForeignKey('users.id'))16vacation_property_id = db.Column(db.Integer, db.ForeignKey('vacation_properties.id'))17guest = db.relationship("User", back_populates="reservations")18vacation_property = db.relationship("VacationProperty", back_populates="reservations")1920def __init__(self, message, vacation_property, guest):21self.message = message22self.guest = guest23self.vacation_property = vacation_property24self.status = 'pending'2526def confirm(self):27self.status = 'confirmed'2829def reject(self):30self.status = 'rejected'3132def __repr__(self):33return '<VacationProperty %r %r>' % self.id, self.name3435def notify_host(self):36self._send_message(self.vacation_property.host.phone_number,37render_template('messages/sms_host.txt',38name=self.guest.name,39description=self.vacation_property.description,40message=self.message))4142def notify_guest(self):43self._send_message(self.guest.phone_number,44render_template('messages/sms_guest.txt',45description=self.vacation_property.description,46status=self.status))4748def _get_twilio_client(self):49return Client(account_sid(), auth_token())5051def _send_message(self, to, message):52self._get_twilio_client().messages.create(53to=to,54from_=phone_number(),55body=message)
Next, let's see how to handle incoming messages from Twilio webhooks.
This method handles the Twilio request triggered by the host's SMS and does three things:
In the Twilio console, you should change the 'A Message Comes In' webhook to call your application's endpoint in the route /confirm:
One way to expose your machine to the world during development is using ngrok. Your URL for the SMS web hook on your phone number should look something like this:
1http://<subdomain>.ngrok.io/confirm2
An incoming request from Twilio comes with some helpful parameters. These include the From
phone number and the message Body
.
We'll use the From
parameter to look up the host and check if they have any pending reservations. If they do, we'll use the message body to check for the message 'accepted' or 'rejected'. Finally, we update the reservation status and use the SmsNotifier
abstraction to send an SMS to the guest telling them the host accepted or rejected their reservation request.
In our response to Twilio, we'll use Twilio's TwiML Markup Language to command Twilio to send an SMS notification message to the host.
airtng_flask/views.py
1from airtng_flask import db, bcrypt, app, login_manager2from flask import session, g, request, flash, Blueprint3from flask_login import login_user, logout_user, current_user, login_required4from twilio.twiml.voice_response import VoiceResponse56from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \7ReservationConfirmationForm8from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params9from airtng_flask.models import init_models_module1011init_models_module(db, bcrypt, app)1213from airtng_flask.models.user import User14from airtng_flask.models.vacation_property import VacationProperty15from airtng_flask.models.reservation import Reservation161718@app.route('/', methods=["GET", "POST"])19@app.route('/register', methods=["GET", "POST"])20def register():21form = RegisterForm()22if request.method == 'POST':23if form.validate_on_submit():2425if User.query.filter(User.email == form.email.data).count() > 0:26form.email.errors.append("Email address already in use.")27return view('register', form)2829user = User(30name=form.name.data,31email=form.email.data,32password=form.password.data,33phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data)34)35db.session.add(user)36db.session.commit()37login_user(user, remember=True)3839return redirect_to('home')40else:41return view('register', form)4243return view('register', form)444546@app.route('/login', methods=["GET", "POST"])47def login():48form = LoginForm()49if request.method == 'POST':50if form.validate_on_submit():51candidate_user = User.query.filter(User.email == form.email.data).first()5253if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,54form.password.data):55form.password.errors.append("Invalid credentials.")56return view('login', form)5758login_user(candidate_user, remember=True)59return redirect_to('home')60return view('login', form)616263@app.route('/logout', methods=["POST"])64@login_required65def logout():66logout_user()67return redirect_to('home')686970@app.route('/home', methods=["GET"])71@login_required72def home():73return view('home')747576@app.route('/properties', methods=["GET"])77@login_required78def properties():79vacation_properties = VacationProperty.query.all()80return view_with_params('properties', vacation_properties=vacation_properties)818283@app.route('/properties/new', methods=["GET", "POST"])84@login_required85def new_property():86form = VacationPropertyForm()87if request.method == 'POST':88if form.validate_on_submit():89host = User.query.get(current_user.get_id())9091property = VacationProperty(form.description.data, form.image_url.data, host)92db.session.add(property)93db.session.commit()94return redirect_to('properties')9596return view('property_new', form)979899@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})100@app.route('/reservations/<property_id>', methods=["GET", "POST"])101@login_required102def new_reservation(property_id):103vacation_property = None104form = ReservationForm()105form.property_id.data = property_id106107if request.method == 'POST':108if form.validate_on_submit():109guest = User.query.get(current_user.get_id())110111vacation_property = VacationProperty.query.get(form.property_id.data)112reservation = Reservation(form.message.data, vacation_property, guest)113db.session.add(reservation)114db.session.commit()115116reservation.notify_host()117118return redirect_to('properties')119120if property_id is not None:121vacation_property = VacationProperty.query.get(property_id)122123return view_with_params('reservation', vacation_property=vacation_property, form=form)124125126@app.route('/confirm', methods=["POST"])127def confirm_reservation():128form = ReservationConfirmationForm()129sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."130131user = User.query.filter(User.phone_number == form.From.data).first()132reservation = Reservation \133.query \134.filter(Reservation.status == 'pending'135and Reservation.vacation_property.host.id == user.id) \136.first()137138if reservation is not None:139140if 'yes' in form.Body.data or 'accept' in form.Body.data:141reservation.confirm()142else:143reservation.reject()144145db.session.commit()146147sms_response_text = "You have successfully {0} the reservation".format(reservation.status)148reservation.notify_guest()149150return twiml(_respond_message(sms_response_text))151152153# controller utils154@app.before_request155def before_request():156g.user = current_user157uri_pattern = request.url_rule158if current_user.is_authenticated and (159uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):160redirect_to('home')161162163@login_manager.user_loader164def load_user(id):165try:166return User.query.get(id)167except:168return None169170171def _respond_message(message):172response = VoiceResponse()173response.message(message)174return response
Congratulations!
You've just learned how to automate your workflow with Twilio Programmable SMS. In the next pane, we'll look at some other features you can add.
To improve upon this you could add anonymous communications so that the host and guest could communicate through a shared Twilio phone number: Call Masking with Python and Flask.
You might also enjoy these other tutorials:
Create a seamless customer service experience by building an IVR (Interactive Voice Response) Phone Tree for your company.
Convert web traffic into phone calls with the click of a button.