Simple Calendar With Events In Python Flask (Free Download)

Welcome to a tutorial on how to create a simple calendar with events in Python Flask. So you want to create a calendar in Python Flask, only to find complicated ones all over the Internet? Well, here’s a quick step-by-step sharing of my own simple version – 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

Source code on GitHub gist

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 FLASK CALENDAR

All right, let us now get into the details of how to create a simple calendar in 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
  • 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) EVENTS TABLE

S1A_events.sql
CREATE TABLE events (
  id INTEGER,
  start DATETIME,
  end DATETIME,
  text TEXT NOT NULL,
  color TEXT NOT NULL,
  bg TEXT NOT NULL,
  PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE INDEX idx_start ON events (start);
CREATE INDEX idx_end ON events (end);

First, we are going to need a database table to store the calendar event. Here is a simple one.

  • id Event ID, primary key.
  • start end The start and end dates of the event.
  • text Details of the event.
  • color bg Text and background color.

 

1B) CREATE THE DATABASE

S1B_create.sql
# (A) LOAD PACKAGES
import sqlite3, os
from sqlite3 import Error

# (B) DATABASE + SQL FILE
DBFILE = "events.db"
SQLFILE = "S1A_events.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!")

This script pretty much reads the above SQL file and creates the actual database file.

 

 

STEP 2) EVENTS LIBRARY

S2_lib.py
# (A) LOAD SQLITE MODULE
import sqlite3
from calendar import monthrange
DBFILE = "events.db"

# (B) SAVE EVENT
def save (start, end, txt, color, bg, id=None):
  # (B1) CONNECT
  conn = sqlite3.connect(DBFILE)
  cursor = conn.cursor()

  # (B2) DATA & SQL
  data = (start, end, txt, color, bg,)
  if id is None:
    sql = "INSERT INTO `events` (`start`, `end`, `text`, `color`, `bg`) VALUES (?,?,?,?,?)"
  else:
    sql = "UPDATE `events` SET `start`=?, `end`=?, `text`=?, `color`=?, `bg`=? WHERE `id`=?"
    data = data + (id,)

  # (B3) EXECUTE
  cursor.execute(sql, data)
  conn.commit()
  conn.close()
  return True

# (C) DELETE EVENT
def delete(id):
  # (C1) CONNECT
  conn = sqlite3.connect(DBFILE)
  cursor = conn.cursor()

  # (C2) EXECUTE
  cursor.execute("DELETE FROM `events` WHERE `id`=?", (id,))
  conn.commit()
  conn.close()
  return True

# (D) GET EVENTS
def get(month, year):
  # (D1) CONNECT
  conn = sqlite3.connect(DBFILE)
  cursor = conn.cursor()

  # (D2) DATE RANGE CALCULATIONS
  daysInMonth = str(monthrange(year, month)[1])
  month = month if month>=10 else "0" + str(month)
  dateYM = str(year) + "-" + str(month) + "-"
  start = dateYM + "01 00:00:00"
  end = dateYM + daysInMonth + " 23:59:59"

  # (D3) GET EVENTS
  cursor.execute(
    "SELECT * FROM `events` WHERE ((`start` BETWEEN ? AND ?) OR (`end` BETWEEN ? AND ?) OR (`start` <= ? AND `end` >= ?))",
    (start, end, start, end, start, end)
  )
  rows = cursor.fetchall()
  if len(rows)==0:
    return None

  # s & e : start & end date
  # c & b : text & background color
  # t : event text
  data = {}
  for r in rows:
    data[r[0]] = {
      "s" : r[1], "e" : r[2],
      "c" : r[4], "b" : r[5],
      "t" : r[3]
    }
  return data

Next, we are going to need a library to work with the database. There are only 3 simple functions in this one:

  • save() Add or update an event.
  • delete() Delete an event.
  • get() Get all events for a given month/year.

 

 

STEP 3) FLASK SERVER

S3_server.py
# (A) INIT
# (A1) LOAD MODULES
import sys
import S2_lib as evt
from flask import Flask, request, render_template, make_response
 
# (A2) FLASK SETTINGS + INIT
HOST_NAME = "localhost"
HOST_PORT = 80
app = Flask(__name__)
# app.debug = True

# (B) ROUTES
# (B1) CALENDAR PAGE
@app.route("/", methods=["GET", "POST"])
def index():
  return render_template("S4A_calendar.html")

# (B2) ENDPOINT - GET EVENTS
@app.route("/get/", methods=["POST"])
def get():
  data = dict(request.form)
  events = evt.get(int(data["month"]), int(data["year"]))
  return "{}" if events is None else events

# (B3) ENDPOINT - SAVE EVENT
@app.route("/save/", methods=["POST"])
def save():
  data = dict(request.form)
  ok = evt.save(data["s"], data["e"], data["t"], data["c"], data["b"], data["id"] if "id" in data else None)
  msg = "OK" if ok else sys.last_value
  return make_response(msg, 200)

# (B4) ENDPOINT - DELETE EVENT
@app.route("/delete/", methods=["POST"])
def delete():
  data = dict(request.form)
  ok = evt.delete(data["id"])
  msg = "OK" if ok else sys.last_value
  return make_response(msg, 200)

# (C) START
if __name__ == "__main__":
  app.run(HOST_NAME, HOST_PORT)

Now for the Flask server itself.

  • / Serves the HTML calendar page.
  • /get/ An endpoint to get all events for a given month/year.
  • /save/ Add or update an event.
  • /delete/ Delete an event.

 

STEP 4) CALENDAR PAGE

4A) THE HTML

templates/S4A_calendar.html
<!-- (A) PERIOD SELECTOR -->
<div id="calHead">
  <div id="calPeriod">
    <input id="calToday" type="button" value="&#9733;">
    <input id="calBack" type="button" class="mi" value="&lt;">
    <select id="calMonth"></select>
    <input id="calYear" type="number">
    <input id="calNext" type="button" class="mi" value="&gt;">
  </div>
  <input id="calAdd" type="button" class="mi" value="+">
</div>
 
<!-- (B) CALENDAR WRAPPER -->
<div id="calWrap">
  <div id="calDays"></div>
  <div id="calBody"></div>
</div>
 
<!-- (C) EVENT FORM -->
<dialog id="calForm"><form method="dialog">
  <div id="evtCX">X</div>
  <h2 class="evt100">CALENDAR EVENT</h2>
  <div class="evt50">
    <label>Start</label>
    <input id="evtStart" type="datetime-local" required>
  </div>
  <div class="evt50">
    <label>End</label>
    <input id="evtEnd" type="datetime-local" required>
  </div>
  <div class="evt50">
    <label>Text Color</label>
    <input id="evtColor" type="color" value="#000000" required>
  </div>
  <div class="evt50">
    <label>Background Color</label>
    <input id="evtBG" type="color" value="#ffdbdb" required>
  </div>
  <div class="evt100">
    <label>Event</label>
    <input id="evtTxt" type="text" required>
  </div>
  <div class="evt100">
    <input type="hidden" id="evtID">
    <input type="button" id="evtDel" value="Delete">
    <input type="submit" id="evtSave" value="Save">
  </div>
</form></dialog>

Look no further, this is the only HTML page in the entire project.

  1. <div id="calHead"> The page header.
    • <div id="calPeriod"> Month and year selector.
    • <input id="calAdd"> Add a new event.
  2. <div id="calWrap"> Calendar wrapper.
    • <div id="calDays"> Day names (Monday to Sunday)
    • <div id="calBody"> The calendar itself.
  3. <dialog id="calForm"> A hidden popup event form.

 

 

4B) JAVASCRIPT – PROPERTIES & HELPERS

static/S4B_calendar.js
var cal = {
  // (A) PROPERTIES
  // (A1) FLAGS & DATA
  mon : false, // monday first
  events : null, // events data for current month/year
  sMth : 0, // selected month
  sYear : 0, // selected year
  sDIM : 0, // number of days in selected month
  sF : 0, // first date of the selected month (yyyymmddhhmm)
  sL : 0, // last date of the selected month (yyyymmddhhmm)
  sFD : 0, // first day of the selected month (mon-sun)
  sLD : 0, // last day of the selected month (mon-sun)
  ready : 0, // to track loading
 
  // (A2) HTML ELEMENTS
  hMth : null, hYear : null, // month & year
  hCD : null, hCB : null, // calendar days & body
  hFormWrap : null, hForm : null, // event form
  hfID : null, hfStart : null, // event form fields
  hfEnd : null, hfTxt : null,
  hfColor : null, hfBG : null,
  hfDel : null,
 
  // (A3) HELPER FUNCTION - TRANSITION
  transit : swap => {
    if (document.startViewTransition) { document.startViewTransition(swap); }
    else { swap(); }
  },
 
  // (B) SUPPORT FUNCTION - AJAX FETCH
  ajax : (req, data, onload) => {
    // (B1) FORM DATA
    let form = new FormData();
    for (let [k,v] of Object.entries(data)) { form.append(k,v); }
 
    // (B2) FETCH
    fetch(req + "/", { method:"POST", body:form })
    .then(res => res.text())
    .then(txt => onload(txt))
    .catch(err => console.error(err));
  },
  // ...
};

The Javascript is massive and starts with a load of flags, data, and helper functions. Not going to explain everything line by line, but the noteworthy:

  • var cal = {} Contains all the calendar mechanics.
  • mon : true/false  To set Monday or Sunday first.
  • cal.ajax() A helper function to do an AJAX call.

 

4C) JAVASCRIPT – INIT

static/S4B_calendar.js
// (C) INIT CALENDAR
init : () => {
  // (C1) GET HTML ELEMENTS
  cal.hMth = document.getElementById("calMonth");
  cal.hYear = document.getElementById("calYear");
  cal.hCD = document.getElementById("calDays");
  cal.hCB = document.getElementById("calBody");
  cal.hFormWrap = document.getElementById("calForm");
  cal.hForm = cal.hFormWrap.querySelector("form");
  cal.hfID = document.getElementById("evtID");
  cal.hfStart = document.getElementById("evtStart");
  cal.hfEnd = document.getElementById("evtEnd");
  cal.hfTxt = document.getElementById("evtTxt");
  cal.hfColor = document.getElementById("evtColor");
  cal.hfBG = document.getElementById("evtBG");
  cal.hfDel = document.getElementById("evtDel");

  // (C2) MONTH & YEAR SELECTOR
  let now = new Date(), nowMth = now.getMonth() + 1;
  for (let [i,n] of Object.entries({
    1 : "January", 2 : "Febuary", 3 : "March", 4 : "April",
    5 : "May", 6 : "June", 7 : "July", 8 : "August",
    9 : "September", 10 : "October", 11 : "November", 12 : "December"
  })) {
    let opt = document.createElement("option");
    opt.value = i;
    opt.innerHTML = n;
    if (i==nowMth) { opt.selected = true; }
    cal.hMth.appendChild(opt);
  }
  cal.hYear.value = parseInt(now.getFullYear());

  // (C3) ATTACH CONTROLS
  cal.hMth.onchange = cal.load;
  cal.hYear.onchange = cal.load;
  document.getElementById("calBack").onclick = () => cal.pshift();
  document.getElementById("calNext").onclick = () => cal.pshift(1);
  document.getElementById("calAdd").onclick = () => cal.show();
  cal.hForm.onsubmit = () => cal.save();
  document.getElementById("evtCX").onclick = () => cal.transit(() => cal.hFormWrap.close());
  cal.hfDel.onclick = cal.del;

  // (C4) DRAW DAY NAMES
  let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
  if (cal.mon) { days.push("Sun"); } else { days.unshift("Sun"); }
  for (let d of days) {
    let cell = document.createElement("div");
    cell.className = "calCell";
    cell.innerHTML = d;
    cal.hCD.appendChild(cell);
  }

  // (C5) LOAD & DRAW CALENDAR
  cal.load();
},
// ...
window.onload = cal.init;

cal.init() runs on page load. Looks intimidating, but all it does is:

  • Get the HTML elements.
  • Setup the month and year selector.
  • Setup the day names.
  • “Enable” the calendar interface.

 

 

4D) JAVASCRIPT – PERIOD SHIFT

static/S4B_calendar.js
// (D) SHIFT CURRENT PERIOD BY 1 MONTH
pshift : forward => {
  cal.sMth = parseInt(cal.hMth.value);
  cal.sYear = parseInt(cal.hYear.value);
  if (forward) { cal.sMth++; } else { cal.sMth--; }
  if (cal.sMth > 12) { cal.sMth = 1; cal.sYear++; }
  if (cal.sMth < 1) { cal.sMth = 12; cal.sYear--; }
  cal.hMth.value = cal.sMth;
  cal.hYear.value = cal.sYear;
  cal.load();
},

// (E) JUMP TO TODAY
today : () => {
  let now = new Date(), ny = now.getFullYear(), nm = now.getMonth()+1;
  if (ny!=cal.sYear || (ny==cal.sYear && nm!=cal.sMth)) {
    cal.hMth.value = nm;
    cal.hYear.value = ny;
    cal.load();
  }
},
  • cal.pshift() A small helper function to drive the last/next month buttons. It simply adds/minus one to the current month.
  • cal.today() Jumps back to the current month/year.

 

4E) JAVASCRIPT – LOAD & DRAW HTML CALENDAR

static/S4B_calendar.js
// (F) LOAD EVENTS
load : () => {
  // (F1) SET SELECTED PERIOD
  cal.sMth = parseInt(cal.hMth.value);
  cal.sYear = parseInt(cal.hYear.value);
  cal.sDIM = new Date(cal.sYear, cal.sMth, 0).getDate();
  cal.sFD = new Date(cal.sYear, cal.sMth-1, 1).getDay();
  cal.sLD = new Date(cal.sYear, cal.sMth-1, cal.sDIM).getDay();
  let m = cal.sMth;
  if (m < 10) { m = "0" + m; } cal.sF = parseInt(String(cal.sYear) + String(m) + "010000"); cal.sL = parseInt(String(cal.sYear) + String(m) + String(cal.sDIM) + "2359"); // (D2) AJAX GET EVENTS cal.ajax("get", { month : cal.sMth, year : cal.sYear }, evt => {
    cal.events = JSON.parse(evt);
    cal.draw();
  });
},
 
// (G) DRAW CALENDAR
draw : () => {
  // (G1) CALCULATE DAY MONTH YEAR
  // note - jan is 0 & dec is 11 in js
  // note - sun is 0 & sat is 6 in js
  let now = new Date(), // current date
      nowMth = now.getMonth()+1, // current month
      nowYear = parseInt(now.getFullYear()), // current year
      nowDay = cal.sMth==nowMth && cal.sYear==nowYear ? now.getDate() : null ;

  // (G2) DRAW CALENDAR ROWS & CELLS
  // (G2-1) INIT + HELPER FUNCTIONS
  let rowA, rowB, rowC, rowMap = {}, rowNum = 1, cell, cellNum = 1,
 
  // (G2-2) HELPER - DRAW A NEW ROW
  rower = () => {
    rowA = document.createElement("div");
    rowB = document.createElement("div");
    rowC = document.createElement("div");
    rowA.className = "calRow";
    rowA.id = "calRow" + rowNum;
    rowB.className = "calRowHead";
    rowC.className = "calRowBack";
    cal.hCB.appendChild(rowA);
    rowA.appendChild(rowB);
    rowA.appendChild(rowC);
  },
 
  // (G2-3) HELPER - DRAW A NEW CELL
  celler = day => {
    cell = document.createElement("div");
    cell.className = "calCell";
    if (day) {
      cell.innerHTML = day;
      cell.classList.add("calCellDay");
      cell.onclick = () => {
        cal.show();
        let d = +day, m = +cal.hMth.value,
            s = `${cal.hYear.value}-${String(m<10 ? "0"+m : m)}-${String(d<10 ? "0"+d : d)}T00:00:00`;
        cal.hfStart.value = s;
        cal.hfEnd.value = s;
      };
    }
    rowB.appendChild(cell);
    cell = document.createElement("div");
    cell.className = "calCell";
    if (day===undefined) { cell.classList.add("calBlank"); }
    if (day!==undefined && day==nowDay) { cell.classList.add("calToday"); }
    rowC.appendChild(cell);
  };
 
  // (G2-4) RESET CALENDAR
  cal.hCB.innerHTML = ""; rower();

  // (G2-5) BLANK CELLS BEFORE START OF MONTH
  if (cal.mon && cal.sFD != 1) {
    let blanks = cal.sFD==0 ? 7 : cal.sFD ;
    for (let i=1; i<blanks; i++) { celler(); cellNum++; }
  }
  if (!cal.mon && cal.sFD != 0) {
    for (let i=0; i<cal.sFD; i++) { celler(); cellNum++; }
  }

  // (G2-6) DAYS OF THE MONTH
  for (let i=1; i<=cal.sDIM; i++) {
    rowMap[i] = { r : rowNum, c : cellNum };
    celler(i);
    if (cellNum%7==0 && i!=cal.sDIM) { rowNum++; rower(); }
    cellNum++;
  }
  
  // (G2-7) BLANK CELLS AFTER END OF MONTH
  if (cal.mon && cal.sLD != 0) {
    let blanks = cal.sLD==6 ? 1 : 7-cal.sLD;
    for (let i=0; i<blanks; i++) { celler(); cellNum++; }
  }
  if (!cal.mon && cal.sLD != 6) {
    let blanks = cal.sLD==0 ? 6 : 6-cal.sLD;
    for (let i=0; i<blanks; i++) { celler(); cellNum++; }
  }
 
  // (G3) DRAW EVENTS
  if (Object.keys(cal.events).length > 0) { for (let [id,evt] of Object.entries(cal.events)) {
    // (G3-1) EVENT START & END DAY
    let sd = new Date(evt.s), ed = new Date(evt.e);
    if (sd.getFullYear() < cal.sYear) { sd = 1; }
    else { sd = sd.getMonth()+1 < cal.sMth ? 1 : sd.getDate(); }
    if (ed.getFullYear() > cal.sYear) { ed = cal.sDIM; }
    else { ed = ed.getMonth()+1 > cal.sMth ? cal.sDIM : ed.getDate(); }

    // (G3-2) "MAP" ONTO HTML CALENDAR
    cell = {}; rowNum = 0;
    for (let i=sd; i<=ed; i++) {
      if (rowNum!=rowMap[i]["r"]) {
        cell[rowMap[i]["r"]] = { s:rowMap[i]["c"], e:0 };
        rowNum = rowMap[i]["r"];
      }
      if (cell[rowNum]) { cell[rowNum]["e"] = rowMap[i]["c"]; }
    }
 
    // (G3-3) DRAW HTML EVENT ROW
    for (let [r,c] of Object.entries(cell)) {
      let o = c.s - 1 - ((r-1) * 7), // event cell offset
          w = c.e - c.s + 1; // event cell width
      rowA = document.getElementById("calRow"+r);
      rowB = document.createElement("div");
      rowB.className = "calRowEvt";
      rowB.innerHTML = cal.events[id]["t"];
      rowB.style.color = cal.events[id]["c"];
      rowB.style.backgroundColor = cal.events[id]["b"];
      rowB.classList.add("w"+w);
      if (o!=0) { rowB.classList.add("o"+o); }
      rowB.onclick = () => cal.show(id);
      rowA.appendChild(rowB);
    }
  }}
},
  • cal.load() Fetch events from the database.
  • cal.draw() Draw the HTML calendar (and the events on it).

 

4F) JAVASCRIPT – SHOW EVENT FORM

static/S4B_calendar.js
// (H) SHOW EVENT FORM
show : id => {
  if (id) {
    cal.hfID.value = id;
    cal.hfStart.value = cal.events[id]["s"];
    cal.hfEnd.value = cal.events[id]["e"];
    cal.hfTxt.value = cal.events[id]["t"];
    cal.hfColor.value = cal.events[id]["c"];
    cal.hfBG.value = cal.events[id]["b"];
    cal.hfDel.style.display = "inline-block";
  } else {
    cal.hForm.reset();
    cal.hfID.value = "";
    cal.hfDel.style.display = "none";
  }
  cal.transit(() => cal.hFormWrap.show());
},

cal.show() is used to show the hidden popup event form.

  • id===NUMBER When the user clicks on an event to edit it. Set the form fields with the selected event and open the popup.
  • id===NOT NUMBER When the user clicks on “add event”. Reset the form and open the popup.

 

4G) JAVASCRIPT – SAVE EVENT

static/S4B_calendar.js
// (I) SAVE EVENT
save : () => {
  // (I1) COLLECT DATA
  // s & e : start & end date
  // c & b : text & background color
  // t : event text
  var data = {
    s : cal.hfStart.value.replace("T", " "),
    e : cal.hfEnd.value.replace("T", " "),
    t : cal.hfTxt.value,
    c : cal.hfColor.value,
    b : cal.hfBG.value
  };
  if (cal.hfID.value != "") { data.id = parseInt(cal.hfID.value); }

  // (I2) DATE CHECK
  if (new Date(data.s) > new Date(data.e)) {
    alert("Start date cannot be later than end date!");
    return false;
  }

  // (I3) SAVE
  cal.ajax("save", data, res => {
    if (res=="OK") {
      cal.transit(() => cal.hFormWrap.close());
      cal.load();
    } else { alert(res); }
  });
  return false;
},

This should be self-explanatory. Get the data from the HTML form, and send it to the server for saving.

 

4H) JAVASCRIPT – DELETE EVENT

static/S4B_calendar.js
// (J) DELETE EVENT
del : () => { if (confirm("Delete Event?")) {
  cal.ajax("delete", { id : parseInt(cal.hfID.value) }, res => {
    if (res=="OK") {
      cal.transit(() => cal.hFormWrap.close());
      cal.load();
    } else { alert(res); }
  });
}}

Finally, yet another self-explanatory function – Delete a selected event.

 

EXTRAS

That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.

 

SQLITE IS NOT GREAT

Before the haters start banging on their keyboards, we are using SQLite here only for convenience. While it is simple and easy for learning beginners, it is not great for live systems; It is lacking in performance, and cannot support distributed/cloud setups. So pick up a “better database” on your own – MYSQL, Oracle, POSTGRES, MSSQL, etc…

 

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!

2 thoughts on “Simple Calendar With Events In Python Flask (Free Download)”

Leave a Comment

Your email address will not be published. Required fields are marked *