
Verwendung von Python und Flask zur Verwaltung von Warteschlangen per SMS
Während viele menschliche Erfahrungen durch den Einsatz von Technologie verbessert werden können, gibt es eine, die eindeutig reif für eine moderne Lösung ist: das Warten in der Schlange. Ganz gleich, ob Sie Ihren Führerschein verlängern oder einen Platz in einem beliebten Brunchlokal ergattern wollen, die Zeit, die Sie in Wartezimmern mit Fremden verbringen, würden Sie wahrscheinlich gerne vermeiden. Glücklicherweise werden dank der Allgegenwart von Mobiltelefonen neue text- und webbasierte Warteschlangensysteme immer häufiger eingesetzt. Senden Sie eine SMS, um Ihren Platz in der Schlange zu reservieren, sich über den aktuellen Status zu informieren und sich benachrichtigen zu lassen, wenn Sie den Anfang der Schlange erreicht haben.
In diesem Tutorial werde ich Ihnen zeigen, wie Sie Python und die Flask Framework ein einfaches System zur Verwaltung von SMS-Warteschlangen aufbauen kann. Es besteht aus drei Hauptkomponenten:
Ein Backend, um auf Textnachrichten zu reagieren und entsprechende Maßnahmen zu ergreifen
Eine Statusseite, auf die über das Internet zugegriffen werden kann oder die an einem Kiosk angezeigt wird
Eine Verwaltungsseite, die die in der Warteschlange stehenden Personen auflistet und die Möglichkeit bietet, sie zu benachrichtigen oder zu entfernen
Um die Dinge einfach zu halten, wird diese Anwendung nur die grundlegendsten Funktionen behandeln, aber das Grundgerüst, das sie bietet, sollte es einfach machen, ein robustes System zu entwickeln, das Ihren Anforderungen entspricht.
Voraussetzungen
Vonage API-Konto
Um dieses Tutorial durchzuführen, benötigen Sie ein Vonage API-Konto. Wenn Sie noch keines haben, können Sie sich noch heute anmelden und mit einem kostenlosen Guthaben beginnen. Sobald Sie ein Konto haben, finden Sie Ihren API-Schlüssel und Ihr API-Geheimnis oben auf dem Vonage-API-Dashboard.
In diesem Lernprogramm wird auch eine virtuelle Telefonnummer verwendet. Um eine zu erwerben, gehen Sie zu Rufnummern > Rufnummern kaufen und suchen Sie nach einer Nummer, die Ihren Anforderungen entspricht.
Einrichten
Um zu beginnen, erstellen Sie ein neues Verzeichnis für das Projekt und wechseln Sie zu diesem:
Wenn Sie lieber mit dem endgültigen Code beginnen möchten, können Sie das Beispielprojekt von der Nexmo Community GitHub klonen:
Sobald Sie sich in Ihrem Projektverzeichnis befinden, erstellen und aktivieren Sie eine virtuelle Umgebung:
Die virtuelle Umgebung hilft Ihnen, Ihre Projektabhängigkeiten zu verwalten und zu isolieren. Um die notwendigen Abhängigkeiten für dieses Projekt zu installieren, benötigen Sie eine requirements.txt Datei. Wenn Sie das Beispiel-Repository geklont haben, sind Sie bereits bereit, aber wenn Sie von Grund auf neu bauen, sollte Ihre Datei so aussehen:
Flask==1.1.1
Flask-SQLAlchemy==2.4.1
nexmo==2.4.0Um diese Abhängigkeiten zu installieren, führen Sie den folgenden Befehl in Ihrem Projektverzeichnis aus:
Datenbank-Initialisierung
Bevor Ihre Anwendung ordnungsgemäß ausgeführt werden kann, müssen Sie eine Datenbank einrichten, um Informationen über die Wartenden zu speichern. Da die Anforderungen an die Datenspeicherung für dieses Projekt relativ einfach sind, werden Sie die in Python integrierte Datenbank SQLite verwenden. Sie werden Ihre Datenbank mit Flask-SQLAlchemyverwalten, das eine Schicht über der Datenbank bereitstellt, so dass Sie Abfragen mit einfachen Funktionen durchführen können, anstatt SQL zu schreiben.
Wenn Sie den Code nicht aus dem Beispiel-Repository heruntergeladen haben, erstellen Sie eine neue Datei namens main.pyan, die Folgendes enthält:
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)Dieses Skript richtet Ihre Flask-Anwendung und die grundlegende Datenbankkonfiguration ein, einschließlich der Festlegung eines Namens und eines Speicherorts für die Datenbank sowie der Definition der Namen und Datentypen der Spalten innerhalb der Datenbank. Sobald Sie dieses oder das Beispiel main.pyhaben, starten Sie Python im interaktiven Modus:
Führen Sie dann die folgenden Befehle aus, um Ihre Datenbank einzurichten:
ngrok und Nexmo Konfiguration
Nachdem Sie nun eine Datenbank eingerichtet haben, müssen Sie einige grundlegende Einstellungen vornehmen, um Nexmo zum Senden und Empfangen von Textnachrichten zu verwenden. Als ersten Schritt müssen Sie sicherstellen, dass ngrok installiert ist und auf Port 5000 läuft:
Wenn ngrok läuft, wird Ihnen eine URL angezeigt, die Sie für die Weiterleitung zu Ihrem lokalen Server verwenden können. Notieren Sie sich diese URL, da Sie sie bald verwenden werden:

Suchen Sie auf der Hauptseite Ihres Nexmo Dashboards nach Ihrem Schlüssel und Ihrem Geheimnis:

Kopieren Sie diese und setzen Sie sie an den Anfang Ihrer main.py Datei wie folgt ein:
NEXMO_KEY = "<Your Nexmo Key>"
NEXMO_SECRET = "<Your Nexmo Secret>"
Fügen Sie dann diese Zeile hinzu:
client = nexmo.Client(key=NEXMO_KEY, secret=NEXMO_SECRET)Zurück im Dashboard, navigieren Sie zu Numbers -> Ihre Numbers (wenn Sie noch keine Numbers haben, gehen Sie zu Numbers -> Numbers kaufen zuerst). Bewegen Sie den Mauszeiger über Ihre Numbers, bis Sie das Symbol Kopieren Schaltfläche sehen. Klicken Sie auf diese Schaltfläche und fügen Sie die Nummer in main.py wie folgt ein:
NEXMO_NUMBER = "<Your Nexmo Number>"
Klicken Sie hier auf das Zahnradsymbol unter dem Menüpunkt Verwalten Spalte:

In der Eingehende Webhook-URL geben Sie Folgendes ein und verwenden dabei die ngrok-URL, die Sie zuvor erfasst haben:
<Your ngrok URL>/webhooks/inbound-sms

Speichern Sie diese Einstellung.
Wenn Sie mit dem Beispiel-Repository arbeiten, haben Sie jetzt alles, was Sie brauchen, um die Anwendung auszuführen. Geben Sie Folgendes in das Terminal ein, um Ihren Entwicklungsserver zu starten:
Navigieren Sie in einem Browser zu Ihrer ngrok-URL, um die Statusansicht zu sehen, oder gehen Sie zu <ngrok url>/list um die Verwaltungsansicht zu sehen. Schreiben Sie dann "Hallo" an die Nummer, die Sie konfiguriert haben, um sich in die Liste einzutragen!
Diejenigen, die von Grund auf neu aufbauen, können nun mit der Erstellung der Backend-Logik fortfahren.
Backend
Die Backend-Struktur für diese Anwendung ist relativ einfach. Sie werden vier Routen erstellen - zwei, die mit den Frontend-Ansichten verbunden sind, einen Webhook für den Empfang eingehender SMS-Nachrichten und einen Stream zur Veröffentlichung von Server-gesendete Ereignisse. Zusätzlich zu den Routen benötigen Sie eine Reihe von Funktionen, die auf der Grundlage der empfangenen Nachrichten und anderer Benutzereingaben Aktionen ausführen. Schließlich benötigen Sie noch einige Hilfsfunktionen, um Datenbankabfragen zu verwalten und Text zu formatieren.
Beginnen wir mit den Routen für die Ansichten. Fügen Sie folgendes zu main.py nach dem Abschnitt, in dem Sie die Datenbank initialisieren, und vor 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)Die index Weg ist einfach - wenn jemand die Website besucht, sieht er den Inhalt der index.html Seite (die noch erstellt werden muss), die Informationen über die Leitungslänge und die anzurufende Telefonnummer enthält (mit Hilfe von Funktionen, die Sie bald schreiben werden).
Die list Route enthält etwas mehr Informationen, da diese Seite Eingaben zulässt. Wenn die Seite aufgerufen wird (eine GET-Anfrage), sieht der Besucher den Inhalt von list.htmlmit den Benutzerinformationen (Telefonnummern der Personen in der Warteschlange). Wenn eine POST-Anfrage an diese Route über das Formular auf der Seite gestellt wird, werden die Informationen aus der Anfrage verwendet, um festzustellen, welche Schaltfläche gedrückt wurde. Die Seite notify, remove, und query_users Funktionen werden in Kürze definiert.
Nun, da Sie diese Routen definiert haben, ist es an der Zeit, die Webhook-Route hinzuzufügen:
@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)Sie kennen diese Route vielleicht schon aus dem Konfigurationsschritt zuvor. Wenn Nexmo eine SMS-Nachricht an die Nummer erhält, die Sie mit diesem Webhook konfiguriert haben, erhalten Sie ein Anfrageobjekt, das den Inhalt der Nachricht und Informationen über den Absender enthält. Für unsere Zwecke ziehen wir die Telefonnummer des Absenders und den Text der Nachricht heraus und ordnen den Text den entsprechenden Aktionsfunktionen zu. Wenn wir die Nachricht nicht analysieren können, teilen wir dies dem Absender mit, damit er es erneut versuchen kann.
Bevor Sie die endgültige Route erstellen, lassen Sie uns unsere Hilfs- und Aktionsfunktionen definieren. Fügen Sie das Folgende in main.py nach den Routendefinitionen ein:
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'])
returnDiese Hilfsfunktionen funktionieren wie folgt:
phone_format: Teilen Sie die konfigurierte Rufnummer zu Anzeigezwecken mit Bindestrichen auf.query_length: Abfrage der Datenbank, um festzustellen, wie viele Personen in der Warteschlange stehen.query_users: Abfrage der Datenbank nach allen Nutzern und Formatierung des Ergebnisses, so dass die Wartenden und die bereits benachrichtigten Nutzer separat gruppiert werden.send: Senden Sie eine SMS-Nachricht mit der angegebenen Nummer und dem Text.
Fügen Sie dann diese Aktionsfunktionen hinzu:
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
Zusammengefasst:
add: Fügt einen neuen Benutzer hinzu, wenn 'Hi' gesendet wird. Gibt auch den Status des Benutzers zurück, wenn er bereits in der Warteschlange ist.remove: Entfernt einen Benutzer aus der Liste. Kann über eine Schaltfläche auf der Verwaltungsseite ausgelöst werden oder wenn der Benutzer "Abbrechen" schreibt.notify: Weist den Benutzer darauf hin, dass er an der Reihe ist. Ausgelöst durch eine Schaltfläche auf der Verwaltungsseite. Berechnet auch die Gesamtwartezeit für diesen Benutzer und speichert sie in der Datenbank.status: Zeigt dem Benutzer an, an welcher Stelle er sich in der Schlange befindet.help: Bietet grundlegende Informationen über Befehle, die der Benutzer senden kann.
Mit diesen definierten Routen und Funktionen haben Sie fast alles, was Sie für ein vollständiges Backend benötigen. Die vierte und letzte Route gibt uns die Möglichkeit, die Ansichten auf der Grundlage von Nachrichten vom Server dynamisch zu aktualisieren, was nützlich ist, wenn Personen per SMS zur Liste hinzugefügt oder von ihr entfernt werden:
@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")Diese Route funktioniert anders als die vorherigen. Nach dem ersten Aufruf durch den Client läuft die Funktion weiter, bis der Server herunterfährt. Dadurch kann sie eine Verbindung mit dem Client aufrechterhalten und Aktualisierungen vornehmen, sobald sich die Anzahl der Personen in der Warteschlange ändert.
Nun, da Sie alle Backend-Teile haben, ist es an der Zeit, die Ansichten zu erstellen!
Frontend
Sie haben zwei Ansichten zu erstellen: eine für die Statusseite und eine für die Verwaltungsseite. Beginnen Sie mit der Erstellung eines neuen Verzeichnisses, templatesund eine neue Datei innerhalb dieses Verzeichnisses, index.html. Geben Sie in dieser Datei Folgendes ein:
<!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>
Hier sind ein paar Dinge zu beachten. Zunächst werden Sie sehen, dass wir Bootstrap verwenden, um einige grundlegende Gestaltungsmöglichkeiten zu schaffen. Sie können mehr über die Verwendung von Bootstrap in einem unserer Beiträgen von vor ein paar Wochen. Zweitens: Beachten Sie die {{ length }} und {{ number }} Felder, in denen wir Eingaben aus dem Backend erhalten, wenn die Vorlage gerendert wird. Schließlich sehen Sie sich den kurzen Javascript-Schnipsel am Ende an - dieser verbindet sich mit unserem stream Endpunkt und verarbeitet vom Server gesendete Ereignisse, wobei die Anzahl der Benutzer in der Warteschlange dynamisch aktualisiert wird.
Außerdem gibt es einen kurzen noscript der so eingestellt ist, dass die Seite alle dreißig Sekunden aktualisiert wird, wenn Javascript deaktiviert wurde. So, wie diese Anwendung eingerichtet wurde, funktioniert sie auch ohne Javascript einwandfrei, sie wird nur nicht so schnell aktualisiert.
Für die Verwaltungsseite erstellen Sie eine neue Datei list.html und fügen Sie Folgendes ein:
<!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>
Auf dieser Seite finden Sie ein Formular, das dazu dient, Daten zu übermitteln, wenn Schaltflächen gedrückt werden. Wie die Statusseite enthält auch diese Seite etwas Javascript für die Verarbeitung von vom Server gesendeten Ereignissen, funktioniert aber auch dann noch ordnungsgemäß, wenn Javascript deaktiviert wurde.
Der Startschuss
Nachdem das Backend und das Frontend fertig sind, können Sie nun Ihre Anwendung ausführen! Wie bereits erwähnt, geschieht dies mit:
Navigieren Sie in einem Browser zu Ihrer ngrok-URL, um die Statusansicht zu sehen:

Gehen Sie zu <ngrok url>/list um die Verwaltungsansicht zu sehen. Schreiben Sie dann "Hallo" an die Nummer, die Sie konfiguriert haben, um sich in die Liste einzutragen!

Nächste Schritte
Es gibt eine Reihe von Möglichkeiten, wie diese Anwendung verbessert werden könnte, um ein stabileres Erlebnis zu bieten. Die naheliegendste ist die Aufnahme einer geschätzten Wartezeit in die Statusaktualisierungen. Die Wartezeiten für diejenigen, die in der Schlange stehen, werden bereits berechnet und zum Sortieren der Liste verwendet, so dass Sie sich nur noch auf Ihren bevorzugten Algorithmus für die Schätzungen einigen müssten. Eine weitere Idee wäre es, mehr Optionen als SMS zu unterstützen, z. B. Facebook Messenger oder WhatsApp. Wenn Sie diesen Weg einschlagen möchten, sollten Sie sich Nexmo's Nachrichten API.
Wenn Sie auf Probleme stoßen oder Fragen haben, wenden Sie sich an uns auf unserem Gemeinschaft Slack. Vielen Dank fürs Lesen!
