Welcome to a tutorial on how to create a simple Python online voting with Flask and SQLite. Want to create a simple voting (or poll) system in Python? Those Django and database stuff is too confusing? Well, here’s a simple version that I have made for beginners – Read on!
TABLE OF CONTENTS
DOWNLOAD & NOTES
Here is the download link to the example code, so you don’t have to copy-paste everything.
EXAMPLE CODE DOWNLOAD
Just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
SORRY FOR THE ADS...
But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.
Buy Me A Coffee Code Boxx eBooks
PYTHON VOTING SYSTEM
All right, let us now get into the details of creating a simple voting system with Python Flask and SQLite.
QUICK SETUP
- Create a virtual environment
virtualenv venv
and activate it –venv\Scripts\activate
(Windows)venv/bin/activate
(Linux/Mac) - Install required libraries –
pip install flask flask-session
- For those who are new, the default Flask folders are –
static
Public files (JS/CSS/images/videos/audio)templates
HTML pages
STEP 1) THE DATABASE
1A) QUESTIONS & OPTIONS
-- (A) QUESTIONS TABLE
CREATE TABLE questions (
qid INTEGER,
txt TEXT NOT NULL,
PRIMARY KEY("qid" AUTOINCREMENT)
);
-- (B) OPTIONS TABLE
CREATE TABLE options (
oid INTEGER,
qid INTEGER NOT NULL,
txt TEXT NOT NULL,
votes INTEGER DEFAULT 0,
PRIMARY KEY("oid" AUTOINCREMENT)
);
CREATE INDEX idx_qid ON options (qid);
-- (C) DUMMY DATA
INSERT INTO questions (txt) VALUES ("What is your favorite meme animal?");
INSERT INTO options (qid, txt, votes) VALUES
(1, "Birb", 11), (1, "Doge", 22), (1, "Cate", 33), (1, "Snek", 44);
The first step of every project, deal with the database foundations (and dummy data for testing). Not to worry, there are only 2 simple tables in this project:
qid |
Question ID. Primary key and auto-increment. |
txt |
The question itself. |
oid |
Option ID. Primary key and auto-increment. |
qid |
Question ID. |
txt |
The option itself. |
votes |
The number of votes for this option. |
1B) CREATE DATABASE TABLE
# (A) LOAD PACKAGES
import sqlite3, os
from sqlite3 import Error
# (B) DATABASE + SQL FILE
DBFILE = "votes.db"
SQLFILE = "S1A_votes.sql"
# (C) DELETE OLD DATABASE IF EXIST
if os.path.exists(DBFILE):
os.remove(DBFILE)
# (D) IMPORT SQL
conn = sqlite3.connect(DBFILE)
with open(SQLFILE) as f:
conn.executescript(f.read())
conn.commit()
conn.close()
print("Database created!")
Next, we simply create a new database and import the above SQL file.
STEP 2) VOTES LIBRARY MODULE
# (A) LOAD SQLITE MODULE
import sqlite3
DBFILE = "votes.db"
# (B) GET QUESTION + OPTIONS + VOTES
def get(qid):
# (B1) RETURNS A DICTIONARY (NONE ON ERROR/NOT FOUND)
res = {
"q" : "", # QUESTION
"o" : {}, # OPTIONS, OID : TEXT
"v" : {} # VOTES, OID : NUMBER OF VOTES
}
# (B2) GET QUESTION
conn = sqlite3.connect(DBFILE)
cursor = conn.cursor()
cursor.execute("SELECT txt FROM questions WHERE qid=?", (qid,))
data = cursor.fetchone()
if data is None:
return None
# (B3) GET OPTIONS
res["q"] = data[0]
cursor.execute("SELECT oid, txt, votes FROM options WHERE qid=?", (qid,))
data = cursor.fetchall()
if len(data)==0:
return None
for row in data:
res["o"][row[0]] = row[1]
res["v"][row[0]] = row[2]
# (B4) RETURN RESULT
conn.close()
return res
# (C) SAVE VOTE
def save(qid, oid, oldid):
# (C1) GET CURRENT VOTES COUNT
conn = sqlite3.connect(DBFILE)
cursor = conn.cursor()
cursor.execute("SELECT oid, votes FROM options WHERE qid=?", (qid,))
data = {}
for r in cursor.fetchall():
data[r[0]] = r[1]
# (C2) UPDATE OLD VOTE COUNT
if oldid is not None:
count = data[oldid] - 1
count = count if count>0 else 0
cursor.execute("UPDATE options SET votes=? WHERE qid=? AND oid=?", (count, qid, oldid,))
# (C3) UPDATE NEW VOTE COUNT
count = data[oid] + 1
cursor.execute("UPDATE options SET votes=? WHERE qid=? AND oid=?", (count, qid, oid,))
# (C4) DONE
conn.commit()
conn.close()
return True
The next piece of the puzzle, create a library module to work with the database. Again, keep calm and look carefully. There are only 2 functions here:
get()
Self-explanatory. Gets the specified question, along with the options and number of votes.save()
Update the vote count. Now, this can be a little hard to grasp without a clear picture of the entire process. But try to follow along:- We will store the user’s selected option in
session["vote"][qid]
. - So for a new user who has not made any votes before,
save(QID, OID, NONE)
will simply increment the option’s vote count. - If the user decides to change the option,
save(QID, OID, OLD OID FROM SESSION)
will decrement the old selected option, then increment the newly selected option.
- We will store the user’s selected option in
STEP 3) FLASK SERVER
3A) INITIALIZE
# (A) INIT
# (A1) LOAD MODULES
from flask import Flask, session, render_template, request
from flask_session import Session
import S2_lib as votes
# (A2) FLASK SETTINGS + INIT
HOST_NAME = "localhost"
HOST_PORT = 80
app = Flask(__name__)
# app.debug = True
app.secret_key = "VERY-SECRET-KEY"
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
The first parts of the Flask server script should be easy enough. We are just loading the required modules, and initializing Flask (to also use sessions).
3B) ROUTES – VOTES PAGE
# (B) VOTES PAGE
@app.route("/", methods=["GET", "POST"])
def index():
# (B1) INIT
qid = 1 # QUESTION ID, FIXED TO 1 IN THIS EXAMPLE
if "vote" not in session: # SESSION - KEEP TRACK OF VOTES MADE
session["vote"] = {}
# (B2) SAVE VOTE ON FORM SUBMIT
if request.method == "POST":
oid = int(request.values.get("vote"))
oldid = None if qid not in session["vote"] else session["vote"][qid]
votes.save(qid, oid, oldid)
session["vote"][qid] = oid
# (B3) GET QUESTION FROM DB
data = votes.get(qid)
if data is None:
return "ERROR!"
data["qid"] = qid
# (B4) RENDER PAGE
return render_template("S4_vote.html", **data)
# (C) START
if __name__ == "__main__":
app.run(HOST_NAME, HOST_PORT)
Look no further, there is only one page here. Just trace along, and it should explain itself:
- (B1) As mentioned earlier, we will use
session["vote"]
to keep the choices that the user has made. - (B3 & B4) Get the question, options, and votes from the database – Pass it to be rendered on the HTML page.
- (B2) When the HTML voting form is submitted, this will update the votes count.
STEP 4) VOTING HTML PAGE
<form method="post" class="poll-docket">
<!-- (A) QUESTION -->
<div class="poll-question">{{ q }}</div>
<!-- (B) OPTIONS -->
{% for oid, txt in o.items() %}
<label class="poll-option">
<input type="radio" name="vote" value="{{ oid }}" required
{% if session["vote"][qid] is defined and oid==session["vote"][qid] %}
checked
{% endif %}>
<span class="poll-text">{{txt}}</span>
<span class="poll-votes">({{v[oid]}})</span>
</label>
{% endfor %}
<!-- (C) SUBMIT -->
<input type="submit" class="poll-go" value="Go!">
</form>
Just the plain old HTML voting form.
EXTRAS
That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.
WORKING WITH MULTIPLE QUESTIONS
There are nearly endless ways to tweak the above script.
- Change the script to adapt the
qid
fromGET
. For example,http://site.com/vote?qid=123
- Or maybe even
POST
. - Use the URL route as the
qid
. For example,http://site.com/vote/456
- Load via AJAX.
- Use the “base URL” to serve an empty page first –
http://site.com/vote
- Load the form via AJAX –
http://site/vote/789
- Use the “base URL” to serve an empty page first –
- Create your own
def showqn(qid) GENERATE HTML TEMPLATE
.
Yep, it’s up to you to decide.
LIMITATIONS
As you can see, this voting system is open to the public and uses the session to track. Anyone can just delete the session cookie and vote multiple times. If you want better “secure votes”, consider this:
- Create a users system.
- Only registered users can vote.
- Create a new votes table, with
Question ID (PRIMARY), User ID (PRIMARY), Option ID
. This way, a user can only vote for one option for a question once. - Then, do security checks before saving the votes.
SQLITE & FILE-BASED SESSIONS ARE NOT GOOD
Before the angry “master code ninja” things start to spill acid – SQLite and file-based sessions are easy, but they are not good for professional use:
- They work on a simple single-server setup but fail horribly on distributed/cloud.
- Remember that the database/session file can only exist on one server. Mirroring these files on all other servers becomes a challenge instead.
- They are not great when it comes to performance-wise.
- Also has reliability and security issues.
So yes, PostgreSQL, MySQL, Redis, or MongoDB – At least pick one of these up. SQLite is a good tool for learning, but not so much for professional use.
LINKS & REFERENCES
THE END
Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!