
Compartir:
Zach es un antiguo Educador de Desarrolladores en Vonage
Utiliza Python y Flask para gestionar colas a través de SMS
Aunque muchas experiencias humanas pueden mejorarse con la aplicación de la tecnología, hay una claramente madura para una solución moderna: hacer cola. Tanto si se trata de renovar el carné de conducir como de conseguir un sitio en un lugar popular para almorzar, el tiempo que pasamos hacinados en salas de espera con extraños es algo que probablemente nos gustaría evitar. Afortunadamente, la omnipresencia de los teléfonos móviles hace que cada vez sean más comunes los nuevos sistemas de colas basados en mensajes de texto y en Internet: envíe un mensaje de texto para reservar su sitio en la cola, compruebe las actualizaciones de estado y reciba una notificación cuando haya llegado al principio de la cola.
En este tutorial, te mostraré cómo utilizar Python y el programa Flask para crear un sencillo sistema de gestión de colas de SMS. Hay tres componentes principales:
Un backend para responder a los mensajes de texto y tomar las medidas oportunas
Una página de estado a la que se puede acceder a través de Internet o en un quiosco.
Una página de gestión que enumera a los que están en cola y permite notificarles o darles de baja
Para simplificar las cosas, esta aplicación sólo gestionará las funciones más básicas, pero el esqueleto que proporciona debería facilitar la creación de un sistema robusto que se adapte a sus necesidades.
Requisitos previos
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.
Puesta en marcha
Para empezar, cree un nuevo directorio para el proyecto y navegue hasta él:
Si prefieres empezar con el código final, puedes clonar el proyecto proyecto de ejemplo de la Comunidad Nexmo GitHub:
Una vez en el directorio de tu proyecto, crea y activa un entorno virtual:
El entorno virtual te ayuda a gestionar y aislar las dependencias de tu proyecto. Para instalar las dependencias necesarias para este proyecto, necesitará un archivo requirements.txt archivo. Si has clonado el repo de ejemplo ya estás listo, pero si estás construyendo desde cero tu archivo debería tener este aspecto:
Flask==1.1.1
Flask-SQLAlchemy==2.4.1
nexmo==2.4.0Para instalar estas dependencias, ejecute lo siguiente en el directorio de su proyecto:
Inicialización de la base de datos
Antes de que tu aplicación pueda ejecutarse correctamente, tendrás que configurar una base de datos para almacenar información sobre las personas que esperan en la cola. Dado que los requisitos de almacenamiento de datos para este proyecto son relativamente simples, utilizarás la base de datos incorporada de Python, SQLite. Gestionarás tu base de datos con Flask-SQLAlchemyque proporciona una capa sobre la base de datos para que puedas hacer consultas con funciones simples en lugar de escribir SQL.
Si no ha descargado el código del repositorio de ejemplos, cree un nuevo archivo llamado main.pyque contenga lo siguiente:
from flask import Flask, render_template, request, Response
from flask_sqlalchemy import SQLAlchemy
db_path = "sqlite:///queue.db"
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = db_path
db = SQLAlchemy(app)
class User(db.Model):
phone_number = db.Column(db.String, unique=True, nullable=False, primary_key=True)
notified = db.Column(db.Integer)
join_time = db.Column(db.DateTime)
wait_time = db.Column(db.Integer)
if __name__ == '__main__':
app.run(debug=True, threaded=True)Este script establece tu aplicación Flask y la configuración básica de la base de datos, incluyendo la definición de un nombre y una ubicación para la base de datos, así como la definición de los nombres y tipos de datos de las columnas dentro de la base de datos. Una vez que tengas esto o el ejemplo main.pyinicia Python en modo interactivo:
A continuación, ejecute los siguientes comandos para configurar su base de datos:
Configuración de ngrok y Nexmo
Ahora que tienes una base de datos lista, necesitarás hacer algunas configuraciones básicas para usar Nexmo para enviar y recibir mensajes de texto. Como primer paso, asegúrate de que tienes ngrok instalado y funcionando en el puerto 5000:
Cuando ngrok se esté ejecutando, se te presentará una URL que deberás utilizar para reenviar a tu servidor local. Tome nota de esta URL, ya que la utilizará pronto:

En la página principal de tu Panel Nexmo, localiza tu Clave y Secreto:

Cópielas y colóquelas cerca de la parte superior de su archivo main.py como sigue:
NEXMO_KEY = "<Your Nexmo Key>"
NEXMO_SECRET = "<Your Nexmo Secret>"
Luego, añade esta línea:
client = nexmo.Client(key=NEXMO_KEY, secret=NEXMO_SECRET)De vuelta al cuadro de mandos, navegue hasta Numbers -> Tus Numbers (si aún no tiene un número, vaya a Numbers -> Comprar números primero). Pasa el ratón por encima de tu número hasta que veas el icono Copiar copiar. Haz clic en él y pega el número en main.py así:
NEXMO_NUMBER = "<Your Nexmo Number>"
Una vez aquí, haga clic en el icono de engranaje situado debajo del botón Gestionar Gestionar:

En la URL de Webhook entrante introduzca lo siguiente, utilizando la URL ngrok que reunió anteriormente:
<Your ngrok URL>/webhooks/inbound-sms

Guarda esta configuración.
Si está trabajando desde el repositorio de ejemplo, ahora tiene todo lo que necesita para ejecutar la aplicación. Introduzca lo siguiente en el terminal para iniciar su servidor de desarrollo:
En un navegador, navegue hasta su URL ngrok para ver la vista de estado, o vaya a <ngrok url>/list para ver la vista de gestión. A continuación, envía un mensaje de texto "Hola" al número que hayas configurado para añadirte a la lista.
Para aquellos que construyen desde cero, vamos a pasar a la creación de su lógica de backend.
Backend
La estructura del backend de esta aplicación es relativamente sencilla. Crearemos cuatro rutas: dos asociadas a las vistas del frontend, un webhook para recibir mensajes SMS entrantes y un stream para publicar eventos enviados por el servidor. eventos enviados por el servidor. Además de las rutas, necesitarás un conjunto de funciones que realicen acciones basadas en los mensajes recibidos y otras entradas del usuario. Por último, tendrás algunas funciones de ayuda para gestionar las consultas a la base de datos y dar formato al texto.
Empecemos con las rutas para las vistas. Añade lo siguiente a main.py después de la sección donde inicializas la base de datos y antes de if __name__ == '__main__':
@app.route('/')
def index():
return render_template('index.html', length=query_length(), number=phone_format(NEXMO_NUMBER))
@app.route('/list', methods=('GET', 'POST'))
def list():
if request.method == 'POST':
if 'notify' in request.form:
notify(request.form['notify'])
elif 'remove' in request.form:
remove(request.form['remove'])
elif 'arrived' in request.form:
remove(request.form['arrived'])
users = query_users()
return render_template('list.html', users=users)La ruta index es sencilla: cuando alguien visite el sitio, verá el contenido de la página index.html (aún por crear), que incluirá información sobre la longitud de la línea y el número de teléfono al que hay que llamar (con la ayuda de funciones que escribirás próximamente).
La list tiene un poco más de información, ya que esta página permite introducir datos. Si se visualiza la página (una petición GET), el visitante verá el contenido de list.htmlcon la información del usuario (números de teléfono de las personas en la cola). Si se realiza una solicitud POST a esta ruta a través del formulario de la página, la información de la solicitud se utilizará para determinar qué botón se ha pulsado. La dirección notify, removey query_users se definirán en breve.
Ahora que ya tienes definidas estas rutas, es el momento de añadir la ruta webhook:
@app.route('/webhooks/inbound-sms', methods=('GET', 'POST'))
def inbound_sms():
if request.is_json:
message = request.get(json())
else:
message = dict(request.form) or dict(request.args)
num = message['msisdn']
text = message['text'].lower()
map = {
"hi": add,
"cancel": remove,
"status": status,
"help": help
}
action = map.get(text)
if action:
action(num)
else:
send(num, "Could not understand. Please try again")
return ('', 204)Puede que reconozcas esta ruta del paso de configuración anterior. Cuando Nexmo recibe un mensaje SMS en el número que ha configurado con este webhook, obtendrá un objeto de solicitud que contiene el contenido del mensaje e información sobre el remitente. Para nuestros propósitos, extraemos el número de teléfono del remitente y el texto del mensaje, asignando el texto a las funciones de acción apropiadas. Si no podemos analizar el mensaje, se lo comunicamos al remitente para que vuelva a intentarlo.
Antes de crear la ruta final, definamos nuestras funciones de ayuda y acción. Incluya lo siguiente en main.py después de las definiciones de ruta:
def phone_format(num):
return format(int(num[:-1]), ",").replace(",", "-") + num[-1]
def query_length():
return User.query.filter(User.notified == 0).count()
def query_users():
users_waiting = []
users_notified = []
for result in User.query.all():
if result.notified == 0:
time_diff = datetime.now() - result.join_time
wait_time = divmod(time_diff.seconds, 60)[0]
user = {"phone_number": str(result.phone_number), "wait_time": wait_time}
users_waiting.append(user)
else:
wait_time = result.wait_time
user = {"phone_number": str(result.phone_number), "wait_time": wait_time}
users_notified.append(user)
users = {"waiting": users_waiting, "notified": users_notified}
return users
def send(num, text):
response = client.send_message({'from': NEXMO_NUMBER, 'to': num, 'text': text})
response = response['messages'][0]
if response['status'] == '0':
print('Sent message', response['message-id'])
else:
print('Error:', response['error-text'])
returnEstas funciones de ayuda actúan de la siguiente manera:
phone_format: Divida el número de teléfono configurado con guiones a efectos de visualización.query_length: Consulta la base de datos para ver cuántas personas están esperando en la cola.query_users: Consulte la base de datos para todos los usuarios y formatee el resultado de manera que los que están esperando en la cola y los que ya han sido notificados se agrupen por separado.send: Envía un SMS con el número y el texto facilitados.
A continuación, añada estas funciones de acción:
def add(num):
if User.query.get(num):
send(num, "Hello again!")
status(num)
else:
user = User(phone_number=num, notified=0, join_time=datetime.now())
db.session.add(user)
db.session.commit()
send(num, "You've been added to the list")
help(num)
return
def remove(num):
user = User.query.get(num)
if user:
db.session.delete(user)
db.session.commit()
send(num, "You've been removed from the list")
else:
print("User not found")
return
def notify(num):
user = User.query.get(num)
if user.notified == 0:
send(num, "Your turn")
user.notified = 1
time_diff = datetime.now() - user.join_time
user.wait_time = divmod(time_diff.seconds, 60)[0]
db.session.commit()
else:
print("User already notified")
return
def status(num):
user = User.query.get(num)
if not user:
send(num, "Not in line")
elif user.notified == 1:
send(num, "Notified")
else:
users = query_users()
users_sorted = sorted(users["waiting"], key = lambda i: i['wait_time'], reverse = True)
i = 0
while i < len(users_sorted):
if users_sorted[i]["phone_number"] == num:
i += 1
break
i += 1
send(num, "Number " + str(i) + " of " + str(len(users_sorted)) + " in line")
return
def help(num):
send(num, "For updates, text 'status'nTo remove yourself from the list, text 'cancel'")
return
Resumiendo:
add: Añade un nuevo usuario cuando se envía 'Hola'. También devolverá el estado del usuario si ya está en línea.remove: Elimina a un usuario de la lista. Puede activarse desde un botón de la página de gestión o si el usuario envía el mensaje "Cancelar".notify: Indica al usuario que es su turno. Se activa desde un botón de la página de gestión. También calcula el tiempo total de espera para ese usuario y lo almacena en la base de datos.status: Indica al usuario en qué lugar de la fila se encuentra.help: Proporciona información básica sobre los comandos que el usuario puede enviar.
Con esas rutas y funciones definidas, tienes casi todo lo que necesitas para un backend completo. La cuarta y última ruta nos ofrece una forma de actualizar dinámicamente las vistas en función de los mensajes del servidor, lo que resulta útil cuando se añaden y eliminan personas de la lista a través de SMS:
@app.route("/stream")
def stream():
def eventStream():
line_length = query_length()
yield "data: {}nn".format(json.dumps(query_users()))
while True:
new_line_length = query_length()
if new_line_length != line_length:
line_length = new_line_length
yield "data: {}nn".format(json.dumps(query_users()))
time.sleep(1)
return Response(eventStream(), mimetype="text/event-stream")Esta ruta funciona de forma diferente a las anteriores. Después de ser llamada inicialmente por el cliente, la función continúa ejecutándose hasta que el servidor se apaga. Esto le permite mantener una conexión con el cliente, enviando actualizaciones cada vez que cambia el número de personas en la cola.
Ahora que ya tienes todas las piezas del backend, ¡es hora de crear las vistas!
Frontend
Tienes que crear dos vistas: una para la página de estado y otra para la página de gestión. Empiece por crear un nuevo directorio templatesy un nuevo archivo dentro de ese directorio, index.html. En ese archivo pon lo siguiente:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
</head>
<body>
<div class="container-fluid center-block text-center">
<div class="row">
<div class="col-lg-12">
<p id="line_length" style="font-size: 20vh">{{ length }}</p>
<p style="font-size: 5vh">in line</p>
</div>
<div class="row">
<div class="col-lg-12" style="height: 20vh">
</div>
<div class="row">
<div class="col-lg-12">
<p class="lead" style="font-size: 7vh">Text <b>Hi</b> to <b>{{ number }}</b> to be added</p>
</div>
</div>
</div>
</body>
<script>
var targetContainer = document.getElementById("line_length");
var eventSource = new EventSource("/stream");
eventSource.onmessage = function(e) {
var users = JSON.parse(e.data)
targetContainer.innerHTML = users.waiting.length;
};
</script>
<noscript>
<meta http-equiv="refresh" content="30">
</noscript>
</html>
Algunas cosas a tener en cuenta aquí. En primer lugar, verás que estamos utilizando Bootstrap para proporcionar algunos estilos básicos. Usted puede leer más sobre el uso de Bootstrap en uno de nuestros mensajes de hace unas semanas. En segundo lugar, observe el {{ length }} y {{ number }} donde obtenemos la información del backend cuando se muestra la plantilla. Por último, vea el breve fragmento de Javascript al final, que se conecta a nuestro punto final stream y procesa los eventos enviados por el servidor, actualizando dinámicamente el número de usuarios en línea.
También hay una breve sección noscript que está configurada para actualizar la página cada treinta segundos si se ha desactivado Javascript. De la forma en que se ha configurado esta aplicación, seguirá funcionando correctamente sin Javascript, sólo que no se actualizará tan instantáneamente.
Para la página de gestión, cree un nuevo archivo list.html e incluya lo siguiente:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
</head>
<body>
<form method="POST" class="center-block text-center" style="width: fit-content">
<h3>In Line</h3>
<table id="notified" class="table table-condensed">
{% for user in users.waiting %}
<tr>
<td>{{ user.phone_number }}</td>
<td><button name="notify" value="{{ user.phone_number }}" class="btn btn-primary btn-xs" type="submit">Notify</button></td>
<td><button name="remove" value="{{ user.phone_number }}" class="btn btn-default btn-xs" type="submit">Remove</button></td>
</tr>
{% endfor %}
</table>
<h3>Notified</h3>
<table class="table table-condensed">
{% for user in users.notified %}
<tr>
<td>{{ user.phone_number }}</td>
<td><button name="arrived" value="{{ user.phone_number }}" class="btn btn-primary btn-xs" type="submit">Arrived</button></td>
</tr>
{% endfor %}
</table>
</form>
</body>
<script>
var targetContainer = document.getElementById("notified");
var eventSource = new EventSource("/stream");
eventSource.onmessage = function(e) {
var user;
var users = JSON.parse(e.data);
users = users.waiting.sort((a, b) => (a.wait_time < b.wait_time) ? 1 : -1)
var user_table = '';
for (user of users){
user_table = user_table + '<tr>'
+ '<td>' + user.phone_number + '</td>'
+ '<td><button name="notify" value="' + user.phone_number + '" class="btn btn-primary btn-xs" type="submit">Notify</button></td>'
+ '<td><button name="remove" value="' + user.phone_number + '" class="btn btn-default btn-xs" type="submit">Remove</button></td>'
+ '</tr>'
}
targetContainer.innerHTML = user_table;
};
</script>
<noscript>
<meta http-equiv="refresh" content="30">
</noscript>
</html>
En esta página, verá que hay un formulario, que se utiliza para enviar datos basándose en los botones que se pulsan. También, como la página de estado, ésta incluye algo de Javascript para procesar eventos enviados por el servidor, pero también continuará funcionando correctamente si Javascript ha sido deshabilitado.
El pistoletazo de salida
Con el backend y el frontend completos, ¡ya puedes ejecutar tu aplicación! Como se mencionó anteriormente, esto se hace con:
En un navegador, navegue hasta su URL ngrok para ver la vista de estado:

Vaya a <ngrok url>/list para ver la vista de gestión. A continuación, envía "Hola" al número que hayas configurado para añadirte a la lista.

Próximos pasos
Esta aplicación podría mejorarse de varias maneras para ofrecer una experiencia más sólida. La más obvia es incluir un tiempo de espera estimado en las actualizaciones de estado. Los tiempos de espera de los que están en la cola ya se calculan y se utilizan para ordenar la lista, así que lo único que habría que hacer es establecer el algoritmo preferido para hacer estimaciones. Otra idea sería admitir más opciones que los SMS, como Facebook Messenger o WhatsApp. Si quieres seguir ese camino, echa un vistazo a Nexmo's Mensajes DE NEXMO.
Si te encuentras con algún problema o tienes alguna pregunta, ponte en contacto con nosotros en nuestra Comunidad Slack. Gracias por leernos.
