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
_32import os_32from airtng_flask.config import config_env_files_32from flask import Flask_32_32from flask_bcrypt import Bcrypt_32from flask_sqlalchemy import SQLAlchemy_32from flask_login import LoginManager_32_32db = SQLAlchemy()_32bcrypt = Bcrypt()_32login_manager = LoginManager()_32_32_32def create_app(config_name='development', p_db=db, p_bcrypt=bcrypt, p_login_manager=login_manager):_32 new_app = Flask(__name__)_32 config_app(config_name, new_app)_32 new_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False_32_32 p_db.init_app(new_app)_32 p_bcrypt.init_app(new_app)_32 p_login_manager.init_app(new_app)_32 p_login_manager.login_view = 'register'_32 return new_app_32_32_32def config_app(config_name, new_app):_32 new_app.config.from_object(config_env_files[config_name])_32_32_32app = create_app()_32_32import 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
_47from airtng_flask.models import app_db_47from airtng_flask.models import bcrypt_47_47db = app_db()_47bcrypt = bcrypt()_47_47_47class User(db.Model):_47 __tablename__ = "users"_47_47 id = db.Column(db.Integer, primary_key=True)_47 name = db.Column(db.String, nullable=False)_47 email = db.Column(db.String, nullable=False)_47 password = db.Column(db.String, nullable=False)_47 phone_number = db.Column(db.String, nullable=False)_47_47 reservations = db.relationship("Reservation", back_populates="guest")_47 vacation_properties = db.relationship("VacationProperty", back_populates="host")_47_47 def __init__(self, name, email, password, phone_number):_47 self.name = name_47 self.email = email_47 self.password = bcrypt.generate_password_hash(password)_47 self.phone_number = phone_number_47_47 def is_authenticated(self):_47 return True_47_47 def is_active(self):_47 return True_47_47 def is_anonymous(self):_47 return False_47_47 def get_id(self):_47 try:_47 return unicode(self.id)_47 except NameError:_47 return str(self.id)_47_47 # Python 3_47_47 def __unicode__(self):_47 return self.name_47_47 def __repr__(self):_47 return '<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
_23from airtng_flask.models import app_db_23_23db = app_db()_23_23_23class VacationProperty(db.Model):_23 __tablename__ = "vacation_properties"_23_23 id = db.Column(db.Integer, primary_key=True)_23 description = db.Column(db.String, nullable=False)_23 image_url = db.Column(db.String, nullable=False)_23_23 host_id = db.Column(db.Integer, db.ForeignKey('users.id'))_23 host = db.relationship("User", back_populates="vacation_properties")_23 reservations = db.relationship("Reservation", back_populates="vacation_property")_23_23 def __init__(self, description, image_url, host):_23 self.description = description_23 self.image_url = image_url_23 self.host = host_23_23 def __repr__(self):_23 return '<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 reservation
vacation_property
the guest is requesting (and associated
host
)
status
of the reservation:
pending
,
confirmed
, or
rejected
airtng_flask/models/reservation.py
_55from airtng_flask.models import app_db, auth_token, account_sid, phone_number_55from flask import render_template_55from twilio.rest import Client_55_55db = app_db()_55_55_55class Reservation(db.Model):_55 __tablename__ = "reservations"_55_55 id = db.Column(db.Integer, primary_key=True)_55 message = db.Column(db.String, nullable=False)_55 status = db.Column(db.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'), default='pending')_55_55 guest_id = db.Column(db.Integer, db.ForeignKey('users.id'))_55 vacation_property_id = db.Column(db.Integer, db.ForeignKey('vacation_properties.id'))_55 guest = db.relationship("User", back_populates="reservations")_55 vacation_property = db.relationship("VacationProperty", back_populates="reservations")_55_55 def __init__(self, message, vacation_property, guest):_55 self.message = message_55 self.guest = guest_55 self.vacation_property = vacation_property_55 self.status = 'pending'_55_55 def confirm(self):_55 self.status = 'confirmed'_55_55 def reject(self):_55 self.status = 'rejected'_55_55 def __repr__(self):_55 return '<VacationProperty %r %r>' % self.id, self.name_55_55 def notify_host(self):_55 self._send_message(self.vacation_property.host.phone_number,_55 render_template('messages/sms_host.txt',_55 name=self.guest.name,_55 description=self.vacation_property.description,_55 message=self.message))_55_55 def notify_guest(self):_55 self._send_message(self.guest.phone_number,_55 render_template('messages/sms_guest.txt',_55 description=self.vacation_property.description,_55 status=self.status))_55_55 def _get_twilio_client(self):_55 return Client(account_sid(), auth_token())_55_55 def _send_message(self, to, message):_55 self._get_twilio_client().messages.create(_55 to=to,_55 from_=phone_number(),_55 body=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
_174from airtng_flask import db, bcrypt, app, login_manager_174from flask import session, g, request, flash, Blueprint_174from flask_login import login_user, logout_user, current_user, login_required_174from twilio.twiml.voice_response import VoiceResponse_174_174from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \_174 ReservationConfirmationForm_174from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params_174from airtng_flask.models import init_models_module_174_174init_models_module(db, bcrypt, app)_174_174from airtng_flask.models.user import User_174from airtng_flask.models.vacation_property import VacationProperty_174from airtng_flask.models.reservation import Reservation_174_174_174@app.route('/', methods=["GET", "POST"])_174@app.route('/register', methods=["GET", "POST"])_174def register():_174 form = RegisterForm()_174 if request.method == 'POST':_174 if form.validate_on_submit():_174_174 if User.query.filter(User.email == form.email.data).count() > 0:_174 form.email.errors.append("Email address already in use.")_174 return view('register', form)_174_174 user = User(_174 name=form.name.data,_174 email=form.email.data,_174 password=form.password.data,_174 phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data)_174 )_174 db.session.add(user)_174 db.session.commit()_174 login_user(user, remember=True)_174_174 return redirect_to('home')_174 else:_174 return view('register', form)_174_174 return view('register', form)_174_174_174@app.route('/login', methods=["GET", "POST"])_174def login():_174 form = LoginForm()_174 if request.method == 'POST':_174 if form.validate_on_submit():_174 candidate_user = User.query.filter(User.email == form.email.data).first()_174_174 if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,_174 form.password.data):_174 form.password.errors.append("Invalid credentials.")_174 return view('login', form)_174_174 login_user(candidate_user, remember=True)_174 return redirect_to('home')_174 return view('login', form)_174_174_174@app.route('/logout', methods=["POST"])_174@login_required_174def logout():_174 logout_user()_174 return redirect_to('home')_174_174_174@app.route('/home', methods=["GET"])_174@login_required_174def home():_174 return view('home')_174_174_174@app.route('/properties', methods=["GET"])_174@login_required_174def properties():_174 vacation_properties = VacationProperty.query.all()_174 return view_with_params('properties', vacation_properties=vacation_properties)_174_174_174@app.route('/properties/new', methods=["GET", "POST"])_174@login_required_174def new_property():_174 form = VacationPropertyForm()_174 if request.method == 'POST':_174 if form.validate_on_submit():_174 host = User.query.get(current_user.get_id())_174_174 property = VacationProperty(form.description.data, form.image_url.data, host)_174 db.session.add(property)_174 db.session.commit()_174 return redirect_to('properties')_174_174 return view('property_new', form)_174_174_174@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})_174@app.route('/reservations/<property_id>', methods=["GET", "POST"])_174@login_required_174def new_reservation(property_id):_174 vacation_property = None_174 form = ReservationForm()_174 form.property_id.data = property_id_174_174 if request.method == 'POST':_174 if form.validate_on_submit():_174 guest = User.query.get(current_user.get_id())_174_174 vacation_property = VacationProperty.query.get(form.property_id.data)_174 reservation = Reservation(form.message.data, vacation_property, guest)_174 db.session.add(reservation)_174 db.session.commit()_174_174 reservation.notify_host()_174_174 return redirect_to('properties')_174_174 if property_id is not None:_174 vacation_property = VacationProperty.query.get(property_id)_174_174 return view_with_params('reservation', vacation_property=vacation_property, form=form)_174_174_174@app.route('/confirm', methods=["POST"])_174def confirm_reservation():_174 form = ReservationConfirmationForm()_174 sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."_174_174 user = User.query.filter(User.phone_number == form.From.data).first()_174 reservation = Reservation \_174 .query \_174 .filter(Reservation.status == 'pending'_174 and Reservation.vacation_property.host.id == user.id) \_174 .first()_174_174 if reservation is not None:_174_174 if 'yes' in form.Body.data or 'accept' in form.Body.data:_174 reservation.confirm()_174 else:_174 reservation.reject()_174_174 db.session.commit()_174_174 sms_response_text = "You have successfully {0} the reservation".format(reservation.status)_174 reservation.notify_guest()_174_174 return twiml(_respond_message(sms_response_text))_174_174_174# controller utils_174@app.before_request_174def before_request():_174 g.user = current_user_174 uri_pattern = request.url_rule_174 if current_user.is_authenticated and (_174 uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):_174 redirect_to('home')_174_174_174@login_manager.user_loader_174def load_user(id):_174 try:_174 return User.query.get(id)_174 except:_174 return None_174_174_174def _respond_message(message):_174 response = VoiceResponse()_174 response.message(message)_174 return 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
_55from airtng_flask.models import app_db, auth_token, account_sid, phone_number_55from flask import render_template_55from twilio.rest import Client_55_55db = app_db()_55_55_55class Reservation(db.Model):_55 __tablename__ = "reservations"_55_55 id = db.Column(db.Integer, primary_key=True)_55 message = db.Column(db.String, nullable=False)_55 status = db.Column(db.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'), default='pending')_55_55 guest_id = db.Column(db.Integer, db.ForeignKey('users.id'))_55 vacation_property_id = db.Column(db.Integer, db.ForeignKey('vacation_properties.id'))_55 guest = db.relationship("User", back_populates="reservations")_55 vacation_property = db.relationship("VacationProperty", back_populates="reservations")_55_55 def __init__(self, message, vacation_property, guest):_55 self.message = message_55 self.guest = guest_55 self.vacation_property = vacation_property_55 self.status = 'pending'_55_55 def confirm(self):_55 self.status = 'confirmed'_55_55 def reject(self):_55 self.status = 'rejected'_55_55 def __repr__(self):_55 return '<VacationProperty %r %r>' % self.id, self.name_55_55 def notify_host(self):_55 self._send_message(self.vacation_property.host.phone_number,_55 render_template('messages/sms_host.txt',_55 name=self.guest.name,_55 description=self.vacation_property.description,_55 message=self.message))_55_55 def notify_guest(self):_55 self._send_message(self.guest.phone_number,_55 render_template('messages/sms_guest.txt',_55 description=self.vacation_property.description,_55 status=self.status))_55_55 def _get_twilio_client(self):_55 return Client(account_sid(), auth_token())_55_55 def _send_message(self, to, message):_55 self._get_twilio_client().messages.create(_55 to=to,_55 from_=phone_number(),_55 body=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:
_10http://<subdomain>.ngrok.io/confirm
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 he or she has any pending reservations. If she does, 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
_174from airtng_flask import db, bcrypt, app, login_manager_174from flask import session, g, request, flash, Blueprint_174from flask_login import login_user, logout_user, current_user, login_required_174from twilio.twiml.voice_response import VoiceResponse_174_174from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \_174 ReservationConfirmationForm_174from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params_174from airtng_flask.models import init_models_module_174_174init_models_module(db, bcrypt, app)_174_174from airtng_flask.models.user import User_174from airtng_flask.models.vacation_property import VacationProperty_174from airtng_flask.models.reservation import Reservation_174_174_174@app.route('/', methods=["GET", "POST"])_174@app.route('/register', methods=["GET", "POST"])_174def register():_174 form = RegisterForm()_174 if request.method == 'POST':_174 if form.validate_on_submit():_174_174 if User.query.filter(User.email == form.email.data).count() > 0:_174 form.email.errors.append("Email address already in use.")_174 return view('register', form)_174_174 user = User(_174 name=form.name.data,_174 email=form.email.data,_174 password=form.password.data,_174 phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data)_174 )_174 db.session.add(user)_174 db.session.commit()_174 login_user(user, remember=True)_174_174 return redirect_to('home')_174 else:_174 return view('register', form)_174_174 return view('register', form)_174_174_174@app.route('/login', methods=["GET", "POST"])_174def login():_174 form = LoginForm()_174 if request.method == 'POST':_174 if form.validate_on_submit():_174 candidate_user = User.query.filter(User.email == form.email.data).first()_174_174 if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,_174 form.password.data):_174 form.password.errors.append("Invalid credentials.")_174 return view('login', form)_174_174 login_user(candidate_user, remember=True)_174 return redirect_to('home')_174 return view('login', form)_174_174_174@app.route('/logout', methods=["POST"])_174@login_required_174def logout():_174 logout_user()_174 return redirect_to('home')_174_174_174@app.route('/home', methods=["GET"])_174@login_required_174def home():_174 return view('home')_174_174_174@app.route('/properties', methods=["GET"])_174@login_required_174def properties():_174 vacation_properties = VacationProperty.query.all()_174 return view_with_params('properties', vacation_properties=vacation_properties)_174_174_174@app.route('/properties/new', methods=["GET", "POST"])_174@login_required_174def new_property():_174 form = VacationPropertyForm()_174 if request.method == 'POST':_174 if form.validate_on_submit():_174 host = User.query.get(current_user.get_id())_174_174 property = VacationProperty(form.description.data, form.image_url.data, host)_174 db.session.add(property)_174 db.session.commit()_174 return redirect_to('properties')_174_174 return view('property_new', form)_174_174_174@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})_174@app.route('/reservations/<property_id>', methods=["GET", "POST"])_174@login_required_174def new_reservation(property_id):_174 vacation_property = None_174 form = ReservationForm()_174 form.property_id.data = property_id_174_174 if request.method == 'POST':_174 if form.validate_on_submit():_174 guest = User.query.get(current_user.get_id())_174_174 vacation_property = VacationProperty.query.get(form.property_id.data)_174 reservation = Reservation(form.message.data, vacation_property, guest)_174 db.session.add(reservation)_174 db.session.commit()_174_174 reservation.notify_host()_174_174 return redirect_to('properties')_174_174 if property_id is not None:_174 vacation_property = VacationProperty.query.get(property_id)_174_174 return view_with_params('reservation', vacation_property=vacation_property, form=form)_174_174_174@app.route('/confirm', methods=["POST"])_174def confirm_reservation():_174 form = ReservationConfirmationForm()_174 sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."_174_174 user = User.query.filter(User.phone_number == form.From.data).first()_174 reservation = Reservation \_174 .query \_174 .filter(Reservation.status == 'pending'_174 and Reservation.vacation_property.host.id == user.id) \_174 .first()_174_174 if reservation is not None:_174_174 if 'yes' in form.Body.data or 'accept' in form.Body.data:_174 reservation.confirm()_174 else:_174 reservation.reject()_174_174 db.session.commit()_174_174 sms_response_text = "You have successfully {0} the reservation".format(reservation.status)_174 reservation.notify_guest()_174_174 return twiml(_respond_message(sms_response_text))_174_174_174# controller utils_174@app.before_request_174def before_request():_174 g.user = current_user_174 uri_pattern = request.url_rule_174 if current_user.is_authenticated and (_174 uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):_174 redirect_to('home')_174_174_174@login_manager.user_loader_174def load_user(id):_174 try:_174 return User.query.get(id)_174 except:_174 return None_174_174_174def _respond_message(message):_174 response = VoiceResponse()_174 response.message(message)_174 return 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 Twilio makes it easy to 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.
Thanks for checking out this tutorial! If you have any feedback to share with us, please hit us up on Twitter and let us know what you're building!