In 2013 72.3% of customers reported using a coupon during a purchase. Yet consumers are increasingly digital and transient. MMS coupons instead of paper printouts provide an alternative way to engage with phone-wielding customers.
In this tutorial we’ll walk through how to build a service to distribute branded MMS coupons using Python and Flask. An MMS coupon will be a message with an image that combines the logo of your choice with a scannable barcode. The resulting text message will appear on the user’s phone as shown in the following image.
When the coupon receiver expands the message they’ll see a full image like this one below.
What You’ll Need
To build the branded coupons app we’ll use the three following technologies.
- Python with Flask, Pillow, requests, PyBarcode for creating and serving coupons
- A Twilio account and an US Twilio number for sending coupons via MMS
- Heroku for deploying the app
If you are anxious to see the results of this tutorial and want to deploy the code right now, try out the app with this Heroku deploy button. All of the code for this tutorial is also stored in an open source GitHub repository.
There are five sections to this coupon app tutorial.
- Create the structure of the coupon-creating Flask web app
- Write code to handle user input and HTML templates
- Use Pillow and PyBarcode to generate a branded coupon image
- Build the code to send the coupon image to a customer via MMS
- Deploy our coupon app to Heroku
Let’s get started incrementally building our coupon generator by creating the structure of our Flask app.
Flask web application structure
First set up the Flask app. If you don’t want to type up this code yourself there’s a tutorial-step-1 tag with these files already created.
In this section we’ll create the following list of files and directories.
- requirements.txt – specifies necessary Python libraries
- app.py – runs the dev server for our Flask app locally
- coupons/ – contains the Flask app that generates and sends coupons
Let’s fill out the dependencies in the requirements.txt file. Each of these dependencies has several other libraries it relies upon which is why this list is shorter than the one you’ll see in the repository.
# web framework for our app Flask==0.10.1 Flask-WTF==0.10.2 # retrieving logos and creating the coupons requests==2.4.1 Pillow==2.5.3 pyBarcode==0.7 # sending MMS twilio==3.6.7 # run the app server with Gunicorn gunicorn==19.1.1
To install these dependencies run the following pip command.
pip install -r requirements.txt
Several directories are needed to store our app code. Create a coupons directory in the base project directory. Two subdirectories are required within coupons: templates and static. Under static we also need css and img subdirectories.
The mkdir command handles the creation subdirectories on Linux or Mac OS X. On Windows you can create the folders and subfolders via Windows File Explorer. The resulting directory structure should look like the following.
Note that Git will not propagate empty folders when pushing to a remote repository. Every folder we just created will have a file in it except for the img/ subdirectory. We need to remedy that for our app to work by placing a file in the folder. Download and save this Twilio logo picture or any .png formatted logo of your choice into the img/ directory.
Our app now has its dependencies in a requirements.txt file and the subdirectories for our Flask app.
Next create an app.py file for running our Flask dev server locally. This file will live in the base directory of our project and contain the following lines.
import os from coupons.app import app if __name__ == '__main__': port = int(os.environ.get("PORT", 5000)) if port == 5000: app.debug = True app.run(host='0.0.0.0', port=port)
The coupons/ subdirectory needs to be made into a Python package. Create an empty file named __init__.py in coupons/.
We also need to create a second app.py file under our coupons/ subdirectory that bootstraps our Flask app. The following code with two stubbed out functions will live in this file.
from flask import Flask, request, render_template, redirect, url_for from .forms import CouponForm twilio_logo_png_url = 'http://www.twilio.com/packages/company/' + \ 'img/logos_downloadable_logobrand.png' app = Flask(__name__, static_url_path='/static') app.config.from_pyfile('config.py') @app.route('/', methods=['GET', 'POST']) def create_coupon(): # we'll replace the following line later in the tutorial return 'stub' @app.route('/confirmation/', methods=['GET']) def coupon_confirmation(): # we'll replace the following line later in the tutorial return 'stub'
This app.py file imports Flask and creates a new coupons web application. The app loads Flask’s configuration from a Python file named config.py.
The file also contains two stubbed functions we will fill out later in this tutorial. The first function create_coupon will display the coupon creation form page as well as handle the data from POST form submissions.
The second function coupon_confirmation will be a confirmation page after the coupon is sent properly.
app.py references a config.py file. Create the config.py file now under the coupons/ subdirectory and put the following code in it to load environment variables for our app.
import os # General Flask app settings DEBUG = os.environ.get('DEBUG', None) SECRET_KEY = os.environ.get('SECRET_KEY', None) COUPON_SAVE_DIR = os.environ.get('COUPON_SAVE_DIR', None) QUALIFIED_MEDIA_URL = os.environ.get('QUALIFIED_MEDIA_URL', 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)
The config.py file looks up Flask settings and Twilio credentials from environment variables set locally on Linux or Windows, so if you’re going to run the app locally go ahead and create those variables now.
COUPON_SAVE_DIR is where coupon images should be temporarily saved. Set COUPON_SAVE_DIR to your app’s coupons code directory plus “/static/img/”. For example, “/app/coupons/static/img/”. If you’re running the app locally, the directory is path to your Flask app plus “/coupons/static/img/”.
QUALIFIED_MEDIA_URL is the fully qualified path for saved coupons. For example, if your app is deployed to Heroku with an app named “mms-coupon-creator”, this value would be set to “http://mms-coupon-creator.herokuapp.com/static/img/”.
Next we need to build a web page that accepts user input so we know how to properly create the coupon.
Handling user input
One more Flask-specific Python file is required that will handle user input. Input fields on the webpage will allow a user to specify what phone number to send the coupon to and optionally the logo, barcode and message text to send with the coupon. The webpage’s input fields will look like the following screenshot.
Our app uses the Flask-WTF library to handle form input. Create a forms.py file under the coupons/ directory and put the following code inside.
from flask_wtf import Form from wtforms import TextField, FileField, validators class CouponForm(Form): phone_number = TextField('Recipient US Phone Number (ex: 2025551234)', validators=[validators.Required(), validators.regexp(u'[0-9]+')]) logo_image_url = TextField('Logo Image URL (optional, .png file)') coupon_text = TextField('Coupon Text (optional, ' + \ 'example: Scan me for 20% off!)') serial_number = TextField('Serial Number (optional, ' + \ 'example: 12849480412)', validators=[validators.regexp(u'[0-9]*')]) def validate(self): if not Form.validate(self): return False if self.logo_image_url.data: # ensure at least ends in .png extension if filled in if not self.logo_image_url.data[-4:].lower() == '.png': return False return True
In the above file we’re handling the required phone number and three optional fields, logo_image_url, coupon_text and serial_number. Some basic validation happens to ensure fields are correct but the validators could certainly be reinforced on an improved coupon creator.
That wraps up the Flask app structure but a few HTML templates are required under the templates directory to create the web user interface. These files come from the Bootstrap project’s Narrow Jumbotron theme customized with our input fields. Each of these files below links to the template file in the GitHub repo.
- base.html – other templates extend this one to have valid HTML
- _formhelpers.html – helper function for rendering form fields and errors
- create_coupon.html– input page for creating coupons
- confirmation.html – confirmation page after coupons are sent
There are also two CSS files under the static/css directory.
- bootstrap.min.css – Twitter bootstrap with custom Flatly theme from Bootswatch
- jumbotron-narrow.css – a few extra CSS rules for centering the coupon creation page
We can run the app now using the python app.py command in the root project directory. However, the webpage will just display “stub” in the browser window. Let’s write the code for generating the coupon images so our app generates coupons and wire it up to our app’s views.
Branded Coupon Generation
We wrote code for the Flask app structure in the first two sections. If you want to start with that code already written, check out the tutorial-step-1 tag.
Code in coupons/app.py calls functions in a file named utils.py file that we’ll create now. While we could write all of the code in app.py, it’s good practice to separate logic out into other functions for easier testing. The following code should go into this utils.py file.
import barcode import requests import uuid from barcode.writer import ImageWriter from PIL import Image from StringIO import StringIO from config import COUPON_SAVE_DIR, QUALIFIED_MEDIA_URL extra_spacing = 50 def open_image_file_from_url(image_file_url): if not image_file_url: return False image_request = requests.get(image_file_url) if image_request.status_code == 200: return Image.open(StringIO(image_request.content)) return False
The above function opens an image file from a URL passed in as a parameter. open_image_file_from_url pulls in an external .png file that will be used to combine with a barcode for the branded coupon.
Within the same utils.py file, write a create_background_image function with the below code that return back a blank image canvas that will be used as a background for our coupon.
def create_background_image(width=640, height=480, rgb=(255, 255, 255)): return Image.new('RGB', (width, height), rgb)
The third function to write in utils.py generates a barcode image. This function will be named generate_barcode_image. It wraps the PyBarcode library and returns back a rendered image. With the return value from this function we have a scannable barcode image that will be pasted by another function onto the blank background image.
def generate_barcode_image(serial_number='1234567890'): return barcode.get('ean13', serial_number, writer=ImageWriter()).render()
Now we get to the meat of the utils.py file. The next function called combine_images_into_coupon takes in our logo image pulled down from a URL specified on our app’s web page form along with the rendered barcode. The output is a URL to our newly created coupon file with both the logo and barcode combined in the same image.
def combine_images_into_coupon(logo_img, barcode_img): bg_width = logo_img.size + barcode_img.size + extra_spacing if logo_img.size > barcode_img.size: bg_height = logo_img.size + extra_spacing else: bg_height = barcode_img.size + extra_spacing bg_img = create_background_image(bg_width, bg_height) logo_offset = (extra_spacing / 2, extra_spacing / 2) try: bg_img.paste(logo_img, logo_offset, logo_img) except: # try without the mask bg_img.paste(logo_img, logo_offset) barcode_offset = (logo_img.size + \ extra_spacing / 3, extra_spacing / 2) bg_img.paste(barcode_img, barcode_offset) return save_image(bg_img)
combine_images_into_coupon is where the logo and generated barcode come together in a single image. We ensure the logo or barcode is within the size of the background image. Then we paste the logo image followed by the barcode image onto the background. If there is transparency in the logo we need to add a mask but if that fails we simply do not add a mask to the image.
One more function for utils.py named save_image is shown below.
def save_image(image): unique_filename = str(uuid.uuid4()) + '.png' image.save(COUPON_SAVE_DIR + unique_filename) return unique_filename
In save_image we save the generated coupon to a unique filename that is created via the uuid library within Python. The coupon image file for our app doesn’t need to be stored long term, it just needs to be saved and served up immediately via a URL for the MMS coupon sending phase.
Barcode generation is possible with the code we wrote above but we also need to wire it into the Flask app. Within the app.py file add the following highlighted code within the create_coupon function and remove the return 'stub' placeholder.
from flask import Flask, request, render_template, redirect, url_for from .forms import CouponForm from .utils import generate_barcode_image, combine_images_into_coupon, \ open_image_file_from_url twilio_logo_png_url = 'http://www.twilio.com/packages/company/' + \ 'img/logos_downloadable_logobrand.png' app = Flask(__name__, static_url_path='/static') app.config.from_pyfile('config.py') @app.route('/', methods=['GET', 'POST']) def create_coupon(): form = CouponForm() if form.validate_on_submit(): serial_number = form.serial_number.data phone_number = form.phone_number.data logo_image_url = form.logo_image_url.data coupon_text = form.coupon_text.data logo_img = open_image_file_from_url(logo_image_url) if not logo_img: logo_img = open_image_file_from_url(twilio_logo_png_url) if serial_number: barcode_img = generate_barcode_image(serial_number) else: barcode_img = generate_barcode_image() coupon_url = combine_images_into_coupon(logo_img, barcode_img) return redirect(url_for('coupon_confirmation')) return render_template('create_coupon.html', form=form)
The above code retrieves user input from form data, downloads a logo image and generates the barcode with the assistance of the functions we wrote earlier.
For the coupon_confirmation function also within app.py, replace the return 'stub' placeholder under with the following two lines.
@app.route('/confirmation/', methods=['GET']) def coupon_confirmation(): form = CouponForm() return render_template('confirmation.html', form=form)
With the above code the user is allowed to create a coupon by submitting a form either on the landing page or the confirmation page.
If we run the app now using python app.py we’ll generate the coupon but now we need to deliver it directly to the user, which we can do using Twilio MMS.
Send coupon via MMS
We now have the code to create coupon images with barcodes. If you want to start with the code to this point in the blog post check out the tutorial-step-2 tag. Now let’s add the code for distributing those coupons via MMS.
Head back to the utils.py file. Add the following two imports at the top as well as an instantiation of the Twilio Python helper library.
import barcode import requests import uuid from barcode.writer import ImageWriter from PIL import Image from StringIO import StringIO from twilio.rest import TwilioRestClient from config import COUPON_SAVE_DIR, QUALIFIED_MEDIA_URL from config import TWILIO_NUMBER client = TwilioRestClient() extra_spacing = 50
A function is required to send the MMS based on user input. Add this code at the bottom of the utils.py file.
def send_coupon_via_mms(coupon_filename, recipient_number, msg_text="Scan me for 20% off!"): to_number = "+1" + recipient_number media_url = QUALIFIED_MEDIA_URL + coupon_filename client.messages.create(to=to_number, from_=TWILIO_NUMBER, body=msg_text, media_url=media_url)
Finally, we’ll call this new function from the end of our create_image function in coupons/app.py. The additional line to send MMS coupon messages is highlighted below.
from flask import Flask, request, render_template, redirect, url_for from .forms import CouponForm from .utils import generate_barcode_image, combine_images_into_coupon, \ open_image_file_from_url from .utils import send_coupon_via_mms twilio_logo_png_url = 'http://www.twilio.com/packages/company/' + \ 'img/logos_downloadable_logobrand.png' app = Flask(__name__, static_url_path='/static') app.config.from_pyfile('config.py') @app.route('/', methods=['GET', 'POST']) def create_coupon(): form = CouponForm() if form.validate_on_submit(): serial_number = form.serial_number.data phone_number = form.phone_number.data logo_image_url = form.logo_image_url.data coupon_text = form.coupon_text.data logo_img = open_image_file_from_url(logo_image_url) if not logo_img: logo_img = open_image_file_from_url(twilio_logo_png_url) if serial_number: barcode_img = generate_barcode_image(serial_number) else: barcode_img = generate_barcode_image() coupon_url = combine_images_into_coupon(logo_img, barcode_img) send_coupon_via_mms(coupon_url, phone_number, coupon_text) return redirect(url_for('coupon_confirmation')) return render_template('create_coupon.html', form=form)
Congratulations! You now you have a fully functioning digital coupon creation and distribution service. Or almost anyways.
Give the app a test on your own machine and you’ll see that it will fail to send the MMS message. This is because Twilio needs a publicly accessibly URL from which it can grab the coupon images. You could use a great service like ngrok to test the app on your local machine, or even better, lets just deploy the app to Heroku.
Deploying the Coupon App
Our app should have the desired functionality, but we need to deploy it otherwise the coupons’ URLs will not be accessible to Twilio. To deploy the app to Heroku we need our code committed in a Git repository and to instruct the service how to use our app with a Procfile. For more information on deploying a Python app with Gunicorn on Heroku check out their official guide.
Create the Procfile in the root directory of our project with the following one-line instruction to run the app via Gunicorn.
web: gunicorn app:app
The master branch of our Git repo contains all the code at this point in our app development. Use the following commands to create a new Heroku app, push your code and set the environment variables.
$ heroku create $ git push heroku master $ heroku config:set SECRET_KEY="super secret key" $ heroku config:set COUPON_SAVE_DIR="/app/coupons/static/img/" $ heroku config:set QUALIFIED_MEDIA_URL="http://heroku-app-name.herokuapp.com/static/img/" $ heroku config:set TWILIO_ACCOUNT_SID="your account SID" $ heroku config:set TWILIO_AUTH_TOKEN="your secret Twilio auth token" $ heroku config:set TWILIO_NUMBER="your MMS Twilio number"
Ensure you’ve set the QUALIFIED_MEDIA_URL with your own Heroku app name and trailing slash on the end of the URL. COUPON_SAVE_DIR also must have a trailing slash at the end of the path.
Head to the new Heroku instance and you’ll see the following input screen.
Now we can send branded coupons with text of the discount to a customer number!
Here’s one with the Twilio logo. This is also the default logo if you choose to not fill in a .png logo during the coupon generation process.
Wrapping it up
Awesome! We’ve combined Python, Flask and Twilio to create a web application that sends digital coupons to customers. Now you’ve got the ability to send MMS coupons to your phone-wielding customers as an alternative to paper coupons that can get lost or be forgotten at home.
This web application can be enhanced in many ways so feel free to fork it and contact me at firstname.lastname@example.org with any questions you have on our new branded MMS coupon app.