The backstory (you can skip this if you want to)
This code stems from a project that I had to do for a presentation for a university class around November 2023. The task was the following:
Create a booking website for online lectures and events with dates
We didn't even have to present the project itself, but things like the development cycle, a requirements analysis, etc. with up to 6 screenshots of the actual website instead.
This also my very first full-stack web app.
Some drawings
Since I did decide to use some diagrams inside my presentation, it may also help to know what I was going for while writing the code.
This is the type of data that I wanted to store:
This turned out to be a mix of an entity-relationship-model and a class diagram
And here is a diagram of the entire tech stack, so you can draw a picture (no pun intended) of what the code did:
THE ACTUAL THING
The link to my project on GitHub:
https://github.com/JupperTV/ReferatGrWeb/tree/english-translation
I do want you to use the english-translation branch because on that branch I translated every comment and string that was in German into English. Barely anthing related to the actual logic has been changed.
The code that I want to submit for review in here are the 3 managers. These modules are almost identical, but they still have their small differences that may be worth reviewing. The 3 managers are an abstraction layer for each CSV file where they have multiple functions to retrieve, insert, update, and delete data
I did not implement some things because I thought that it may not be worth it.
accountmanager.py:
#!/usr/bin/python
# ! IMPORTANT NOTE: I obfuscate the data instead of doing any encryption
# ! because this application was only created for demonstration purposes
from base64 import b64encode, b64decode # Zum Obfuskieren der Daten
import csv # All of the data is stored in CSV files
import re#gex
from typing import Final, Iterable
import uuid # To create unique ids
import errors
# Source: https://regexr.com/3e48o
_REGEX_VALID_EMAIL: Final = r"^\S+@\S+\.\S+$"
_UNSUCCESFUL_MATCH = None
_CSV_PATH: Final[str] = "data"
_CSV_ACCOUNT: Final[str] = f"{_CSV_PATH}\\accounts.csv"
# Static variables inside a class for better readabilty
class CSVHeader:
ACCOUNTID: Final[str] = "id"
EMAIL: Final[str] = "email"
BASE64PASSWORD: Final[str] = "password"
FIRSTNAME: Final[str] = "firstname"
LASTNAME: Final[str] = "lastname"
def AsList() -> list[str]:
return [CSVHeader.ACCOUNTID, CSVHeader.EMAIL, CSVHeader.BASE64PASSWORD,
CSVHeader.FIRSTNAME, CSVHeader.LASTNAME]
# A class so that, as long as app.py and eventmanager.py get the
# accountid and email from GetAccountFromEmail() or
# GetAccountFromToken(), they can get whatever account values they need
# themselves
class Account:
def InitFromDict(dictionary: csv.DictReader | dict[str, str]):
if len(dictionary) < 5:
raise errors.NotEnoughElementsInListError()
d = lambda h: dictionary.get(h) # Less boilerplate
return Account(
accountid=d(CSVHeader.ACCOUNTID), email=d(CSVHeader.EMAIL),
base64password=d(CSVHeader.BASE64PASSWORD),
firstname=d(CSVHeader.FIRSTNAME), lastname=d(CSVHeader.LASTNAME))
def __init__(self, accountid: str, email: str, base64password: str,
firstname: str, lastname: str):
self.accountid = accountid
self.email = email
self.base64password = base64password
self.firstname = firstname
self.lastname = lastname
# region Private functions
# A csv.DictReader basically works like a list[dict[str, str]]
def _getdictreader_() -> csv.DictReader:
# * FUTURE ME: I believe I wasted 5 hours on this
# * Weird Python behaviour:
# The newline parameter in open() is an empty string because
# csv.writer and csv.reader have their own ways of controlling line
# breaks (they just embed a "\r\n" by themselves)
# If the newline parameter is not empty, Windows will turn the
# "\r\n" that was written by csv.writer into "\r\r\n", which results
# in an empty line between every data set.
# Sources:
# - https://stackoverflow.com/a/3348664
# - Footnote in https://docs.python.org/3/library/csv.html?highlight=csv.writer#id3
accountfile_read = open(_CSV_ACCOUNT, "r", newline="")
return csv.DictReader(accountfile_read, delimiter=",")
def _obfuscateText_(text: bytes) -> bytes:
if type(text) is str:
# Unicode instead of UTF-8 because Python 3 uses unicode for
# strings
text = bytes(text, encoding="unicode")
return b64encode(text)
# endregion
# * Note:
# I return an entire instance of Account so that app.py and
# entrymanager.py can get the values they need themselves without me
# always having to create a new function that just returns the value
# that is currently needed.
def GetAccountFromEmail(email: str) -> Account | None:
reader: csv.DictReader = list(_getdictreader_())
account = None
for row in reader:
# * Important Detail:
# row.get(...) is being used instead of row[...] because
# row.get(...) returns `None` if the key, or column, doesn't
# exist.
# This is important because the values of the keys will
# automatically be None if the key/column or even
# the entire row empty is.
# (Python's *BUILT-IN* csv library has some unfortunate
# differences when dealing with line breaks - see comment
# "Weird Python behaviour" in accountmanager.AddAccount)
if row.get(CSVHeader.EMAIL) == email:
return Account.InitFromDict(row)
raise errors.AccountDoesNotExistError()
# Note: accountid == token stored in the cookie
def GetAccountFromToken(token: str) -> Account:
reader: csv.DictReader = _getdictreader_()
for row in reader:
if row.get(CSVHeader.ACCOUNTID) == token:
return Account.InitFromDict(row)
def PasswordsAreEqual(originalpassword: str, obfuscatedpassword: str) -> bool:
return originalpassword == b64decode(obfuscatedpassword).decode()
def LoginIsValid(email: str, originalpassword: str) -> bool:
# accountfile_read = open(_CSV_ACCOUNT, "r", newline="")
# reader: Iterable[dict] = csv.DictReader(accountfile_read, delimiter=",")
reader: csv.DictReader = _getdictreader_()
for row in reader:
# Don't check passwords until emails are the same
if row.get(CSVHeader.EMAIL) != email:
continue
if PasswordsAreEqual(originalpassword=originalpassword,
obfuscatedpassword=row.get(CSVHeader.BASE64PASSWORD)):
return True
return False
# TODO: Test
def UserExists(email: str) -> bool:
reader = _getdictreader_()
for row in reader:
if row.get(CSVHeader.EMAIL) == email:
return True
return False
# TODO: Improve in the future?
def PasswordIsValid(originalpassword: str) -> bool:
if not originalpassword:
return False
return True
# The HTML type "email" does everything for me
def EmailIsValid(email: str) -> bool:
# return re.fullmatch(_REGEX_VALID_EMAIL, email) != _UNSUCCESFUL_MATCH
return True
def SaveInCSV(email, originalpassword, firstname, lastname) -> None:
if not PasswordIsValid(originalpassword):
raise ValueError("Invalid password ")
if not EmailIsValid(email):
raise ValueError("Invalid e-mail")
reader = _getdictreader_()
for row in reader:
if email == row.get(CSVHeader.EMAIL):
raise errors.AccountAlreadyExistsError()
accountid = uuid.uuid4() # Random UUID
with open(_CSV_ACCOUNT, "a", newline='') as accountfile_write:
writer = csv.DictWriter(accountfile_write, fieldnames=CSVHeader.AsList(),
delimiter=",")
# .decode() comes from the bytes class and turns the bytes
# object into a str
passwordToSave = _obfuscateText_(
bytes(originalpassword, "unicode_escape")).decode()
# There could be a better way to save the values from the
# Account object
values = [accountid, email, passwordToSave, firstname, lastname]
writer.writerow(dict(zip(CSVHeader.AsList(), values)))
# TODO
def RemoveAccount():
pass
entrymanager.py:
#!/usr/bin/python
# * What is an Entry?
# An entry is what connects an account with an event.
# If, for example, a user registers for an event, then this is
# is saved as an entry/registration for this event
import csv
from typing import Final, Iterable
import uuid
import eventmanager
import errors
_CSV_PATH: Final[str] = "data"
_CSV_ENTRY: Final[str] = f"{_CSV_PATH}\\entries.csv"
# * Note:
# * I don't have a class for the entries, like I did for the accounts
# * and the events, because a dataset for an entry only consists of 3
# * ids anyway.
class CSVHeader:
ENTRYID: Final[str] = "id"
ACCOUNTID: Final[str] = "accountid"
EVENTID: Final[str] = "eventid"
def AsList() -> list[str]:
return [CSVHeader.ENTRYID, CSVHeader.ACCOUNTID, CSVHeader.EVENTID]
def _getdictreader_() -> csv.DictReader:
entryfile_read = open(_CSV_ENTRY, "r", newline="")
return csv.DictReader(entryfile_read, delimiter=",")
def SaveInCSV(accountid: int, eventid: int) -> None:
entryfile_writer = open(_CSV_ENTRY, "a", newline="")
# A Dictwriter isn't worth it here because I would have to zip the 3
# variables with CSVHeader.AsList() and that's too much work for so
# little
writer = csv.writer(entryfile_writer, delimiter=",")
entryid = uuid.uuid4()
writer.writerow([entryid, accountid, eventid])
def DidAccountAlreadyEnter(accountid, eventid) -> bool:
reader = _getdictreader_()
for row in reader:
if not row.values(): # row is empty
continue
if row.get(CSVHeader.ACCOUNTID) == accountid \
and row.get(CSVHeader.EVENTID) == eventid:
return True
return False
def GetAllEntriedEventsOfAccount(accountid) -> list[eventmanager.Event] | None:
events: list[eventmanager.Event] = eventmanager.GetAllEvents()
reader = _getdictreader_()
if not reader:
return None
entriedevents: list[eventmanager.Event] = []
for row in reader:
if row.values() and row.get(CSVHeader.ACCOUNTID) == accountid:
entriedevents.append(eventmanager.GetEventFromId(row.get(CSVHeader.EVENTID)))
return entriedevents
def DeleteAllEntriesWithEvent(eventid) -> None:
events: list[eventmanager.Event] = eventmanager.GetAllEvents()
reader: csv.DictReader = _getdictreader_()
rowsWithoutEvent: list[dict[str, str]] = []
for row in reader:
if not row.values():
continue
if row.get(CSVHeader.EVENTID) == eventid:
continue
rowsWithoutEvent.append(row)
# * Important Note:
# The file will be deleted immediately after being opened.
# writer.writerows() will completely overwrite it
entryfile_write = open(_CSV_ENTRY, "w", newline="")
writer = csv.DictWriter(entryfile_write, fieldnames=CSVHeader.AsList(),
delimiter=",")
writer.writerow(dict(zip(CSVHeader.AsList(), CSVHeader.AsList())))
writer.writerows(rowsWithoutEvent)
def DeleteEntry(accountid: int, eventid: int) -> None:
reader = _getdictreader_()
newCSV: list[dict[str, str]] = []
for row in reader:
if not row.values():
continue # raise errors.AccountHasNoEntriesError("Only Header")
if row.get(CSVHeader.ACCOUNTID) == accountid and row.get(CSVHeader.EVENTID) == eventid:
continue
newCSV.append(row)
entryfile_write = open(_CSV_ENTRY, "w", newline="")
writer = csv.DictWriter(entryfile_write, fieldnames=CSVHeader.AsList(),
delimiter=",")
writer.writerow(dict(zip(CSVHeader.AsList(), CSVHeader.AsList())))
writer.writerows(newCSV)
eventmanager.py:
#!/usr/bin/python
# * What is an event?
# An event is an event that users can register to
# Examples: Lectures, Videocalls, Meetings
import csv
from typing import Final, Iterable
import time
from datetime import datetime
import uuid
import babel.dates
import errors
import entrymanager
class EventType:
ON_SITE: Final[str] = "onsite"
ONLINE: Final[str] = "online"
class CSVHeader:
EVENTID: Final[str] = "id"
NAME: Final[str] = "name"
EPOCH: Final[str] = "epoch"
EVENTTYPE: Final[EventType] = "type"
ORGANIZER_EMAIL: Final[str] = "organizer"
COUNTRY: Final[str] = "country"
CITY: Final[str] = "city"
ZIPCODE: Final[str] = "zipcode"
STREET: Final[str] = "street"
HOUSENUMBER: Final[str] = "housenumber"
DESCRIPTION: Final[str] = "description"
# csv.DictWriter needs the fieldnames of the CSV file
def AsList() -> list[str]:
return [CSVHeader.EVENTID, CSVHeader.NAME, CSVHeader.EPOCH,
CSVHeader.EVENTTYPE, CSVHeader.ORGANIZER_EMAIL, CSVHeader.COUNTRY,
CSVHeader.CITY, CSVHeader.ZIPCODE, CSVHeader.STREET,
CSVHeader.HOUSENUMBER, CSVHeader.DESCRIPTION]
class Event:
def InitFromDict(dictionary: csv.DictReader | dict[str, str]):
if len(dictionary) < 11:
raise errors.NotEnoughElementsInListError()
d = lambda h: dictionary.get(h) # Less boilerplate
return Event(eventid=d(CSVHeader.EVENTID),
eventname=d(CSVHeader.NAME),
epoch=d(CSVHeader.EPOCH),
eventtype=d(CSVHeader.EVENTTYPE),
organizeremail=d(CSVHeader.ORGANIZER_EMAIL),
country=d(CSVHeader.COUNTRY),
zipcode=d(CSVHeader.ZIPCODE),
city=d(CSVHeader.CITY),
street=d(CSVHeader.STREET),
housenumber=d(CSVHeader.HOUSENUMBER),
description=d(CSVHeader.DESCRIPTION)
)
def __init__(self, eventid: str, eventname: str, epoch: str, eventtype: str,
organizeremail: str, country: str, city: str, zipcode: str,
street: str, housenumber: str, description: str):
self.eventid = eventid
self.eventname = eventname
self.epoch = epoch
self.eventtype=eventtype
self.organizeremail = organizeremail
self.country = country
self.city = city
self.zipcode = zipcode
self.street = street
self.housenumber = housenumber
self.description = description
def __iter__(self):
return iter([
self.eventid, self.eventname, self.epoch, self.eventtype,
self.organizeremail, self.country, self.city, self.zipcode,
self.street, self.housenumber, self.description])
# This only exists for displaying information on the frontend
KEYS_FOR_OUTPUT = ["Event number", "Eventname", "Date", "Event type",
"Organizer e-mail", "Country", "City", "Zipcode", "Street",
"House number", "Description"]
def GetReadableEventType(eventtype: str) -> str:
return "On site" if eventtype == EventType.ON_SITE else "Online"
_CSV_PATH: Final[str] = "data"
_CSV_EVENT: Final[str] = f"{_CSV_PATH}\\events.csv"
def _getdictreader_() -> csv.DictReader:
eventfile_read = open(_CSV_EVENT, "r", newline="")
return csv.DictReader(eventfile_read, delimiter=",")
def EpochToNormalTime(epoch: float | str) -> str:
readabledate = datetime.fromtimestamp(float(epoch))
return babel.dates.format_datetime(readabledate, locale="de_DE",
format="dd.MM.yyyy 'um' HH:mm")
def EpochToInputTime(epoch: float | str):
return time.strftime("%Y-%m-%dT%H:%M", time.localtime(float(epoch)))
def InputTimeToEpoch(inputtime: str):
return float(time.mktime(time.strptime(inputtime, "%Y-%m-%dT%H:%M")))
def GetAllEventsCreatedByOrganizer(organizeremail: str) -> list[Event]:
reader = _getdictreader_()
events: list[Event] = []
for row in reader:
if not row.values():
continue
if row.get(CSVHeader.ORGANIZER_EMAIL) == organizeremail:
events.append(Event.InitFromDict(row))
if not events:
raise errors.AccountHasNoEventsError()
return events
def GetAllEvents() -> list[Event]:
return [Event.InitFromDict(row) for row in _getdictreader_()]
def EventExists(event: Event) -> bool:
reader = _getdictreader_()
for row in reader:
if event == Event.InitFromDict(row):
return True
return False
def GetEventFromId(eventid) -> Event | None:
reader = _getdictreader_()
for row in reader:
if eventid == row.get(CSVHeader.EVENTID):
return Event.InitFromDict(row)
return None
def IsTheSameEvent(*args) -> bool:
reader = list(_getdictreader_())
if not reader:
return False
for row in reader:
# If a user enters the exact same data of another event, then
# the eventids will be the only thing difference.
# That's why they aren't being compared
row.pop(CSVHeader.EVENTID)
for index, header in enumerate(CSVHeader.AsList()):
if args[index] != row.get(header):
return False
return True
def CreateEventFromForm(eventname, epoch: float, eventtype: str, organizeremail,
country, city, zipcode: str, street, housenumber: str,
description: str) -> None:
SaveInCSV(eventid=uuid.uuid4(), eventname=eventname, epoch=epoch,
eventtype=eventtype, organizeremail=organizeremail, country=country,
city=city, zipcode=zipcode, street=street, housenumber=housenumber,
description=description)
def SaveInCSV(eventid, eventname, epoch, eventtype, organizeremail, country, city, zipcode,
street, housenumber, description) -> None:
if IsTheSameEvent(eventname, epoch, eventtype, organizeremail, country, city, zipcode,
street, housenumber, description, eventtype):
raise errors.EventAlreadyExistsError()
eventfile_write = open(_CSV_EVENT, "a", newline="")
# It's not worth it to use a DictWriter just to insert a new dataset
writer = csv.writer(eventfile_write, delimiter=",")
writer.writerow([eventid, eventname, epoch,eventtype, organizeremail,
country, city, zipcode, street, housenumber, description])
def ModifyEvent(event: Event) -> None:
reader: list[dict] = list(_getdictreader_())
for row in reader:
if row.get(CSVHeader.EVENTID) == event.eventid:
for index, header in enumerate(CSVHeader.AsList()):
reader[reader.index(row)][header] = list(event)[index]
break # The event has been found
eventfile_write = open(_CSV_EVENT, "w", newline="")
writer = csv.DictWriter(eventfile_write, fieldnames=CSVHeader.AsList())
writer.writerow(dict(zip(CSVHeader.AsList(), CSVHeader.AsList())))
writer.writerows(reader)
def DeleteEvent(eventid):
# Every other row including the headers, other than the row of the
# event that needs to be deleted, are read and then overwriten to
# the file
reader = _getdictreader_()
newCSV: list[dict[str, str]] = []
for row in reader:
if not row.values(): # row is empty
continue
if row.get(CSVHeader.EVENTID) == eventid:
continue
newCSV.append(row)
eventfile_write = open(_CSV_EVENT, "w", newline="")
writer = csv.DictWriter(eventfile_write, fieldnames=CSVHeader.AsList(),
delimiter=",")
writer.writerow(dict(zip(CSVHeader.AsList(), CSVHeader.AsList())))
writer.writerows(newCSV)
entrymanager.DeleteAllEntriesWithEvent(eventid)
Notes
While I was developing this for my presentation, I did realise why SQL exists how it can save a lot of time. And if you read some of the comments I wrote, you may see that I regretted using the built-in csv library instead of a library dedicated for data like pandas.
So feel free to criticize me for these decisions.
EDIT: I forgot to make the repository public...

