In this article, we will be building an electoral voting app with Python, Flask, and Fauna serverless database, similar to electionrunner.com. An electoral voting app lets users create election polls on the internet and provides functionalities that allow other users to cast their votes based on the options provided by the election creator.
If you are new to the concept of Fauna and serverless databases, read this article to become familiar with the terms associated with the topic: https://dev.to/lordghostx/building-a-telegram-bot-with-python-and-fauna-494i
Getting Started with Fauna
Step 1: Set Up Our Fauna Database
The first thing we need to do is create the database for our electoral voting app in the Fauna dashboard. If you have not created an account on Fauna before now, create one here: https://dashboard.fauna.com/accounts/register
In the dashboard, click on the “NEW DATABASE” button, provide a name for your database then press the SAVE
button.
Step 2: Generate Fauna API Key
We will need to create a Fauna API key to connect the database to our election voting app. To do this, navigate to the security settings on the Fauna sidebar (left side of the screen).
Once you have done this, you will be presented with your API key (hidden here for privacy reasons). The key should be copied as soon as it is generated then stored somewhere easily retrievable.
Step 3: Integrate Fauna into Python
Next, we need to get the Python library for Fauna. It’s available on pip and can be installed with a single line in our terminal.
pip install faunadb
After this is installed, we run the sample code provided in Fauna Python driver docs https://docs.fauna.com/fauna/current/drivers/python.html
from faunadb import query as q
from faunadb.objects import Ref
from faunadb.client import FaunaClient
client = FaunaClient(secret="your-secret-here")
indexes = client.query(q.paginate(q.indexes()))
print(indexes)
The code above shows how the Fauna Python driver connects to a database with its API key and prints the indexes associated with it. The result from running this code is similar to the image below.
Building Our Electoral Voting App
Now that we have successfully integrated our Python script with Fauna, let’s get started with building our electoral voting app. We will build the backend and logic with Flask and our user interface with Flask-Bootstrap since it will be a full-stack web application.
We will be building five (5) web pages for our electoral voting app. These are:
- Register Page
- Login Page
- Create Vote Page
- Submit Vote Page
- View Vote Results Page
Step 1: Set Up Our Flask Server
We need to install the Flask library and Flask-Bootstrap the same way we installed Fauna earlier using pip and our terminal.
pip install flask
pip install flask-bootstrap4
Let’s get a basic server running in Flask that will display the text Hello World!
when opened. Create a new project folder and a Python file with the name app.py
and type the following code inside.
from flask import *
from flask_bootstrap import Bootstrap
from faunadb import query as q
from faunadb.objects import Ref
from faunadb.client import FaunaClient
app = Flask(__name__)
Bootstrap(app)
app.config["SECRET_KEY"] = "APP_SECRET_KEY"
client = FaunaClient(secret="your-secret-here")
@app.route("/")
def index():
return “Hello World!”
if __name__ == "__main__":
app.run(debug=True)
When we run our app.py
file we should get a response quite like the image below
If you get this, you’re on the right track. Open this in your browser http://127.0.0.1:5000
to access the app.
Step 2: Design Our Registration Page
The registration page is where users are provided with a form to create a new account on our application so they can create elections and view results.
First, create a folder named templates
in the same folder as our app.py
. A templates folder is used by Flask to store its HTML files which will be rendered by the server in later parts of the application. Our project folder should resemble the image below:
Create another file named register.html
which will be stored inside the templates folder and save the code below in it.
{% extends "bootstrap/base.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-12">
<div class="jumbotron text-center">
<h2>Online Election Voting with Python and Fauna</h2>
<p>Build a Secure Online Election for Free</p>
</div>
</div>
<div class="col-lg-6">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="text-{{ category }}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Choose Username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Choose Password" required>
</div>
<button type="submit" class="btn btn-primary">Create Account</button>
</form>
</div>
</div>
</div>
{% endblock %}
We also need to create a register
route in our Flask app that will render the template with the registration form. Update your app.py
file with the code below.
from flask import *
from flask_bootstrap import Bootstrap
from faunadb import query as q
from faunadb.objects import Ref
from faunadb.client import FaunaClient
app = Flask(__name__)
Bootstrap(app)
app.config["SECRET_KEY"] = "APP_SECRET_KEY"
client = FaunaClient(secret="your-secret-here")
@app.route("/")
def index():
return redirect(url_for("register"))
@app.route("/register/", methods=["GET", "POST"])
def register():
return render_template("register.html")
if __name__ == "__main__":
app.run(debug=True)
When we run our app.py
file we should get a response close to the image below when we open the register
route in our browser:
Step 3: Build Our Registration Logic
Next, we want to build the functionality that allows users to create accounts on our application in Fauna. To do this, we first need to create a Fauna collection. A collection is similar to SQL tables that contain data with similar characteristics e.g user collection that contain information about users in the database. Login to your Fauna dashboard once again, and start by pressing the NEW COLLECTION
button then provide a name for the collection we want to create.
We also need to create an index for our collection. A Fauna index allows us to browse through data that is stored in a database collection, based on specific attributes. To create one, navigate to the DB Overview
tab on the Fauna sidebar (left side of the screen) then click the NEW INDEX
button.
Supply the name you want to use for your index, set the terms to data.username
as we will be using that variable to reference our users later. Also, remember to select the Unique
attribute for our index, this ensures that we do not have a duplicate in our database entries.
Update the app.py
file with the code below to implement the logic for account creation
import pytz
import hashlib
from datetime import datetime
from flask import *
from flask_bootstrap import Bootstrap
from faunadb import query as q
from faunadb.objects import Ref
from faunadb.client import FaunaClient
app = Flask(__name__)
Bootstrap(app)
app.config["SECRET_KEY"] = "APP_SECRET_KEY"
client = FaunaClient(secret="your-secret-here")
@app.route("/")
def index():
return redirect(url_for("register"))
@app.route("/register/", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = request.form.get("username").strip().lower()
password = request.form.get("password")
try:
user = client.query(
q.get(q.match(q.index("users_index"), username)))
flash("The account you are trying to create already exists!", "danger")
except:
user = client.query(q.create(q.collection("users"), {
"data": {
"username": username,
"password": hashlib.sha512(password.encode()).hexdigest(),
"date": datetime.now(pytz.UTC)
}
}))
flash(
"You have successfully created your account, you can now create online elections!", "success")
return redirect(url_for("register"))
return render_template("register.html")
if __name__ == "__main__":
app.run(debug=True)
We first made a query to Fauna to check if the account we are trying to create currently exists with the get
method of the FQL client which fetches any database entry that matches the username we provided using the Fauna index we created earlier.
client.query(q.get(q.match(q.index(index_name), search_query)))
We also used the create
method of the FQL client to store the username
and password
provided by the user along with account creation date
in our database collection.
client.query(q.create(q.collection(collection_name), data))
When we run our server again then submit the form, we should get a response similar to the image below:
Step 4: Design Our Login Page
The login page is where users are provided with a form to authenticate their accounts on our application so they can manage their elections and create new ones.
Create a file named login.html
which will be stored inside the templates folder and save the code below in it.
{% extends "bootstrap/base.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-12">
<div class="jumbotron text-center">
<h2>Online Election Voting with Python and Fauna</h2>
<p>Login to Manage Your Elections and Create New Ones</p>
</div>
</div>
<div class="col-lg-6">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="text-{{ category }}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Enter Your Username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter Your Password" required>
</div>
<button type="submit" class="btn btn-primary">Authenticate Account</button>
</form>
</div>
</div>
</div>
{% endblock %}
We also need to create a login
route in our Flask app that will render the template with the login form. Add the following code below to your app.py
file:
@app.route("/login/", methods=["GET", "POST"])
def login():
return render_template("login.html")
When we run our app.py
file we should get a response quite close to the image below when we open the login
route in our browser:
Step 5: Build Our Login Logic
Now, we want to write a route that collects usernames and passwords from our form and verify the account exists and the provided information is correct. Update your login
route with the code below:
@app.route("/login/", methods=["GET", "POST"])
def login():
if "user" in session:
return redirect(url_for("dashboard"))
if request.method == "POST":
username = request.form.get("username").strip().lower()
password = request.form.get("password")
try:
user = client.query(
q.get(q.match(q.index("users_index"), username)))
if hashlib.sha512(password.encode()).hexdigest() == user["data"]["password"]:
session["user"] = {
"id": user["ref"].id(),
"username": user["data"]["username"]
}
return redirect(url_for("dashboard"))
else:
raise Exception()
except:
flash(
"You have supplied invalid login credentials, please try again!", "danger")
return redirect(url_for("login"))
return render_template("login.html")
@app.route("/dashboard/", methods=["GET"])
def dashboard():
return "Hello World"
Like we did earlier, we made a query to Fauna to get any account with the username provided in the form, then we verified if the password is correct. If the password is correct, save the user details in our session, else tell the user their credentials are not valid.
Step 6: Design Our Create Election Page
The Create Election page is where users are provided with a form to create new elections on our application.
Create a file named create-election.html
which will be stored inside the templates folder and save the code below in it.
{% extends "bootstrap/base.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-12">
<div class="jumbotron text-center">
<h2>Online Election Voting with Python and Fauna</h2>
<p>Create a New Election</p>
</div>
</div>
<div class="col-lg-6">
<form method="POST">
<div class="form-group">
<label for="title">Election Title</label>
<input type="text" class="form-control" id="title" name="title" placeholder="Enter Election Title" required>
</div>
<div class="form-group">
<label for="voting-options">Voting Options</label>
<textarea class="form-control" rows="5" id="voting-options" name="voting-options" placeholder="Enter the next voting option on a new line" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Create Election</button>
</form>
</div>
</div>
</div>
{% endblock %}
We also need to create a route in our Flask app that will render the template with the create_election
form. Before that, we need to create a decorator that only allows authenticated users to access certain routes on our application. Add the code below to your app.py
file
from functools import wraps
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if "user" not in session:
return redirect(url_for("login"))
return f(*args, **kwargs)
return decorated
Now let’s add the route for creating election secured with our authentication wrapper
@app.route("/dashboard/create-election/", methods=["GET", "POST"])
@login_required
def create_election():
return render_template("create-election.html")
When we run our app.py
file we should get a response similar to the image below when we open the create_election
route in our browser:
Step 7: Build Our Create Election Logic
Now, we want to write a route that collects election titles and options from our form and saves them in our database. To do this, we first need to create a Fauna collection as we did earlier named elections
and an index to browse through our data.
Add the code below to your app.py
file
@app.route("/dashboard/create-election/", methods=["GET", "POST"])
@login_required
def create_election():
if request.method == "POST":
title = request.form.get("title").strip()
voting_options = request.form.get("voting-options").strip()
options = {}
for i in voting_options.split("\n"):
options[i.strip()] = 0
election = client.query(q.create(q.collection("elections"), {
"data": {
"creator": session["user"]["id"],
"title": title,
"voting_options": options,
"date": datetime.now(pytz.UTC)
}
}))
return redirect(url_for("vote", election_id=election["ref"].id()))
return render_template("create-election.html")
@app.route("/election/<int:election_id>/", methods=["GET", "POST"])
def election(election_id):
return "You are accessing election ID " + str(election_id)
Here, we made a query to our Fauna database to create a database entry in our elections
collection with the election details provided by the user. We also created a route that will be responsible for handling the viewing of an election and voting.
Step 8: Design Our Vote Page
The Vote page is where users can view an election that has been created and cast their votes. Create a file named vote.html
which will be stored inside the templates folder and save the code below in it.
{% extends "bootstrap/base.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-12">
<div class="jumbotron text-center">
<h2>Online Election Voting with Python and Fauna</h2>
<p>Cast Your Votes for <strong>'{{ election.title }}'</strong> Election</p>
</div>
</div>
<div class="col-lg-6">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="text-{{ category }}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<div class="form-group">
<label for="voting-options">Voting Options</label>
<select class="form-control" id="voting-options" name="vote" required>
{% for i in election.voting_options %}
<option>{{ i }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">Cast Vote</button>
</form>
</div>
</div>
</div>
{% endblock %}
Also, update your app.py
file with the code below
@app.route("/election/<int:election_id>/", methods=["GET", "POST"])
def vote(election_id):
try:
election = client.query(
q.get(q.ref(q.collection("elections"), election_id)))
except:
abort(404)
return render_template("vote.html", election=election["data"])
When we run our server again, we should get a response quite like the image below when we open the vote
route in our browser:
Step 9: Build Our Voting Logic
Now, we want to write a route that collects votes from our database then update the results. Update your vote
route with the code below:
@app.route("/election/<int:election_id>/", methods=["GET", "POST"])
def vote(election_id):
try:
election = client.query(
q.get(q.ref(q.collection("elections"), election_id)))
except:
abort(404)
if request.method == "POST":
vote = request.form.get("vote").strip()
election["data"]["voting_options"][vote] += 1
client.query(q.update(q.ref(q.collection("elections"), election_id), {
"data": {"voting_options": election["data"]["voting_options"]}}))
flash("Your vote was successfully recorded!", "success")
return redirect(url_for("vote", election_id=election_id))
return render_template("vote.html", election=election["data"])
We made a query to Fauna to update our election results with the update
method of the FQL client which updates any data that is previously stored in our database with the following code:
client.query(q.update(q.ref(q.collection(collection_name), document_id), new_data))
When we run our server again, we should get a response close to the image below when we cast our vote:
Step 10: Build Our View Elections Page
The View Elections page is where election creators can view elections they have created and see their results. Create a file named view-elections.html
which will be stored inside the templates folder and save the code below in it.
{% extends "bootstrap/base.html" %}
{% block content %}
<div class="container justify-content-center">
<div class="row">
<div class="col-lg-12">
<div class="jumbotron text-center">
<h2>Online Election Voting with Python and Fauna</h2>
<p>View Election Results</p>
</div>
</div>
{% for i in elections %}
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">{{ i.data.title }}</h5>
</div>
<div class="card-body">
{% for j in i.data.voting_options %}
<p class="card-text">{{ "{} - {} vote(s)".format(j, i.data.voting_options[j]) }}</p>
{% endfor %}
<a href="{{ url_for('vote', election_id=i.ref.id()) }}" class="btn btn-primary" target="_blank">View Election</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
We will also be updating our dashboard
route in our app.py
file to fetch the elections data from Fauna then render it on the user interface.
@app.route("/dashboard/", methods=["GET"])
@login_required
def dashboard():
elections = client.query(q.paginate(
q.match(q.index("election_index"), session["user"]["id"])))
elections_ref = []
for i in elections["data"]:
elections_ref.append(q.get(q.ref(q.collection("elections"), i.id())))
return render_template("view-elections.html", elections=client.query(elections_ref))
Here, we made a query to Fauna to paginate all database entries in the elections
collection that were created by the current user that is logged in. Then a second query to fetch the data in each of the references returned.
When we run our server again, we should get a response similar to the image below when we open the dashboard route (right after logging in).
Conclusion
In this article, we built an electoral voting application similar to electionrunner.com with Fauna's serverless database and Python. We saw how easy it is to integrate Fauna into a Python application and got the chance to explore some of its core features and functionalities.
The source code of our electoral voting application is available on GitHub. If you have any questions, don't hesitate to contact me on Twitter: @LordGhostX
Top comments (1)
Good post!