DEV Community

LordGhostX
LordGhostX

Posted on

How To Build an Electoral Voting App with Python and Fauna

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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"])
Enter fullscreen mode Exit fullscreen mode

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"])
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
aalphaindia profile image
Pawan Pawar

Good post!