diff --git a/calendar_generation.py b/calendar_generation.py index 838ecf9..bf1029e 100644 --- a/calendar_generation.py +++ b/calendar_generation.py @@ -3,8 +3,19 @@ import datetime import recurring_ical_events from fetchMENSA import getMeals +shortnames = ["mon", "tue", "wed", "thu", "fri", "sat"] +longnames = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"] + def getWeek(weekstart: datetime, file: str, showsat: bool): + """ + Liefert alle Events einer Woche zurück. + Wochenstart wird automatisch auf den Montag der Woche gelegt. + :param weekstart: + :param file: + :param showsat: + :return: + """ if weekstart == "today": start_date = datetime.date.today() else: @@ -42,11 +53,13 @@ def getWeek(weekstart: datetime, file: str, showsat: bool): return eventl, daylist(start_date, showsat) -shortnames = ["mon", "tue", "wed", "thu", "fri", "sat"] -longnames = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"] - - def daylist(weekstart: datetime, showsat: bool): + """ + Gibt die Essen einer Woche zurück. + :param weekstart: + :param showsat: + :return: + """ weekday = weekstart dayl = [] if showsat: diff --git a/fetchDUALIS.py b/fetchDUALIS.py index 819137e..a41acc2 100644 --- a/fetchDUALIS.py +++ b/fetchDUALIS.py @@ -2,6 +2,8 @@ import requests import urllib.parse import time from bs4 import BeautifulSoup +from flask import redirect, url_for +from init import Dualis headers = { 'Cookie': 'cnsc=0', @@ -15,6 +17,12 @@ url = "https://dualis.dhbw.de/scripts/mgrqispi.dll" def checkUser(email: str, password: str): + """ + Erhält von Dualis den Token und Cookie für User. + :param email: + :param password: + :return (Token, Session): + """ s = requests.Session() fpw = urllib.parse.quote(password, safe='', encoding=None, errors=None) fmail = urllib.parse.quote(email, safe='', encoding=None, errors=None) @@ -33,6 +41,13 @@ def checkUser(email: str, password: str): def getKurs(token: int, cookie: str): + """ + Bestimmt aus der ersten Prüfung den Kursbezeichner des Users. + TODO: Umstellen auf Bezeichner INKL. Standort + :param token: + :param cookie: + :return Kurs-Bezeichner: + """ try: headers["Cookie"] = "cnsc=" + cookie token = str(token) @@ -47,18 +62,29 @@ def getKurs(token: int, cookie: str): start = content.find(" ") + 4 end = start + (content[start:].find(" ")) kurs = content[start:end] - except: + except AttributeError: kurs = 0 return kurs def logOut(token: int, cookie: str): + """ + Invalidiert Token und Cookie bei Dualis. + :param token: + :param cookie: + """ headers["Cookie"] = "cnsc=" + cookie requests.request("GET", url + "?APPNAME=CampusNet&PRGNAME=LOGOUT&ARGUMENTS=-N" + str(token) + ", -N001", headers=headers, data={}) def getSem(token: int, cookie: str): + """ + Liefert die Liste aller auf Dualis verfügbaren Semester. + :param token: + :param cookie: + :return Liste [[Semester, Semester-ID], ...]: + """ headers["Cookie"] = "cnsc=" + cookie token = str(token) response = requests.request("GET", url + @@ -76,6 +102,13 @@ def getSem(token: int, cookie: str): def getResults(token, cookie: str, resl: str): + """ + Liefert die Liste aller Prüfungsergebnisse eines Semesters. + :param token: + :param cookie: + :param resl: + :return [[Name, Note, Credits], ...]: + """ headers["Cookie"] = "cnsc=" + cookie response = requests.request("GET", url + "?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N" + token + ",-N000307," + ",-N" + resl, headers=headers, data={}) @@ -95,6 +128,12 @@ def getResults(token, cookie: str, resl: str): def getPruefung(url): + """ + Ermittelt Noten "geschachtelter" Prüfungen, die nicht auf der Hauptseite angezeigt werden. + TODO: Namen der spezifischen Prüfungen auch zurückgeben, um Zusammensetzung zu spezifizieren. + :param url: + :return: + """ response = requests.request("GET", "https://dualis.dhbw.de" + url, headers=headers, data={}) html = BeautifulSoup(response.content.decode("utf-8"), 'lxml') table = html.find('table') @@ -111,7 +150,30 @@ def getPruefung(url): def checkLifetime(timecode: float): + """ + Dualis-Token laufen nach 30 Minuten ab. + True, wenn Token noch gültig ist. + False wenn ungültig. + :param timecode: + :return: + """ if time.time() - timecode > 1800: return False else: return True + + +def timeOut(dualis: Dualis, cookie: str, origin: str): + """ + Checkt, ob Token und Cookie noch valide/vorhanden sind. + False, falls alles richtig ist. + Weiterleitung zum Login, falls nicht. + :param dualis: + :param cookie: + :param origin: + :return: + """ + if not checkLifetime(dualis.token_created) or not cookie: + return redirect(url_for("login", next=url_for(origin), code=10)) + else: + return False diff --git a/fetchMENSA.py b/fetchMENSA.py index add62f4..1189008 100644 --- a/fetchMENSA.py +++ b/fetchMENSA.py @@ -8,6 +8,13 @@ nomeal = 'Essen nicht (mehr) verfügbar' def getMeals(day: datetime): + """ + Liefert alle Mahlzeiten eines Tages. \n + Befinden sie sich schon in der Datenbank, werden diese zurückgegeben. \n + Wenn nicht, wird getMealsFromAPI() aufgerufen. \n + :param day: + :return [Name1, Name2, ...]: + """ day = formatDay(day) essen = [] query = Meals.query.filter_by(date=day).all() @@ -20,6 +27,14 @@ def getMeals(day: datetime): def getMealsFromAPI(day: str, dbentry: bool = False): + """ + Fragt die Mensa-API nach den Mahlzeiten eines Tages ab. \n + Wenn dbentry: Schreibt die Ergebnisse in die Datenbank. \n + TODO: Andere Mensen berücksichtigen. + :param day: + :param dbentry: + :return [Name1, Name2, ...]: + """ url = "https://dh-api.paulmartin.cloud/plans/" + day + "?canteens=erzberger" response = requests.request("GET", url) response = response.content @@ -64,6 +79,11 @@ def getMealsFromAPI(day: str, dbentry: bool = False): def pricetofloat(price: str): + """ + Konvertiert den Preis-String der Mensa-API zu einem Python-Float. + :param price: + :return float: + """ price = price[:-2] price = price.replace(",", ".") try: @@ -73,6 +93,12 @@ def pricetofloat(price: str): def formatDay(day: datetime): + """ + Füllt ein Datum mit Nullen auf. \n + "2023-1-1" → "2023-01-01". + :param day: + :return str: + """ if day.day < 10: tag = "0" + str(day.day) else: @@ -83,6 +109,10 @@ def formatDay(day: datetime): @scheduler.task('cron', id="refreshMeals", hour='8-11', day_of_week='*', minute='15', week='*', second='30') def refreshMeals(): + """ + Aktualisiert immer vormittags alle Mahlzeiten in der Datenbank. \n + Datenbankeinträge werden ersetzt, wenn die API andere Mahlzeiten liefert. + """ print("Aktualisiere Essenspläne...\n") with scheduler.app.app_context(): table = Meals.query.all() diff --git a/fetchRAPLA.py b/fetchRAPLA.py index 42593f6..c039fb4 100644 --- a/fetchRAPLA.py +++ b/fetchRAPLA.py @@ -7,6 +7,13 @@ from init import scheduler def parseURL(url: str): + """ + Konvertiert URLs ins korrekte Format. \n + Konvertiert werden: http, www.; page=calendar \n + In: https; page=ical + :param url: + :return str: + """ rapla = url.find("rapla.") if rapla == -1: return 0 @@ -28,6 +35,13 @@ def parseURL(url: str): def getNewRapla(url: str): + """ + Speichert den iCal eines Raplas auf dem Server. \n + Gibt Namen der Datei zurück. \n + TODO: Standort zu Dateibezeichner hinzufügen, um Konflikte zu vermeiden. + :param url: + :return str: + """ url = parseURL(url) if url == 0: return 0 @@ -49,6 +63,11 @@ def getNewRapla(url: str): def getIcal(kurs: str): + """ + Liefert den Namen der Datei des mitgegebenen Kurses. + :param kurs: + :return str: + """ file = open("calendars/list.json", "r") jf = json.load(file) try: @@ -58,6 +77,10 @@ def getIcal(kurs: str): def getRaplas(): + """ + Liefert alle auf dem Server gespeicherten Raplas. + :return (Kursliste, Dateiliste, URL-Liste): + """ file = open("calendars/list.json", "r") jsonf = json.load(file) kursl = [] @@ -72,6 +95,9 @@ def getRaplas(): @scheduler.task("interval", id="refreshRapla", minutes=30) def refreshRapla(): + """ + Aktualisiert alle 30 Minuten alle gespeicherten Raplas. + """ filel = getRaplas()[1] urll = getRaplas()[2] for i in range(len(filel)): diff --git a/get_mysql.py b/get_mysql.py index d5e6622..a760737 100644 --- a/get_mysql.py +++ b/get_mysql.py @@ -2,6 +2,10 @@ import getpass def get_mysql(): + """ + Extrahiert die MySQL-Anmeldedaten aus ~/.my.cnf . \n + Funktioniert wahrscheinlich nur auf Linux, vor allem für den Server gedacht. + """ u = getpass.getuser() f = open("/home/"+u+"/.my.cnf", "r") i = f.read() diff --git a/init-sql.sh b/init-sql.sh index 0467c21..9717c89 100755 --- a/init-sql.sh +++ b/init-sql.sh @@ -1,4 +1,5 @@ #!/bin/sh +#Erstellt alle benötigten MySQL-Tabellen. mysql -e "USE paulmrtn_DUALHUB; CREATE TABLE user ( id int NOT NULL, email VARCHAR(255), password VARCHAR(255), name VARCHAR(255), kurs VARCHAR (15), PRIMARY KEY (ID), UNIQUE (ID, EMAIL) );" mysql -e "USE paulmrtn_DUALHUB; CREATE TABLE dualis ( uid int NOT NULL, token VARCHAR(255), result_list VARCHAR(15), token_created INT, PRIMARY KEY (uid));" diff --git a/init.py b/init.py index f799146..f6266e3 100644 --- a/init.py +++ b/init.py @@ -8,6 +8,10 @@ from flask_apscheduler import APScheduler def create(): + """ + Erstellt die Flask-App inkl. Datenbank und Login-Manager. + :return app: + """ app = Flask(__name__) dbpw = get_mysql()[1] dbun = get_mysql()[0] @@ -34,6 +38,9 @@ db = SQLAlchemy() class User(UserMixin, db.Model): + """ + Datenbank-Modell für User. + """ id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(255), unique=True) password = db.Column(db.String(255)) @@ -42,6 +49,9 @@ class User(UserMixin, db.Model): class Dualis(db.Model): + """ + Datenbank-Modell für Dualis. + """ token = db.Column(db.String(255), unique=True) uid = db.Column(db.Integer, primary_key=True) token_created = db.Column(db.Integer) @@ -49,6 +59,9 @@ class Dualis(db.Model): class Meals(db.Model): + """ + Datenbank-Modell für Meals. + """ id = db.Column(db.Integer, primary_key=True) date = db.Column(db.Date) name = db.Column(db.String(100)) diff --git a/requesthelpers.py b/requesthelpers.py index 85ffe36..9762fb7 100644 --- a/requesthelpers.py +++ b/requesthelpers.py @@ -1,8 +1,10 @@ - - - -def getCookie (cookies): +def getCookie(cookies): + """ + Liefert (letzten) Cookie der Cookies-Liste zurück. + :param cookies: + :return: + """ cookie = 0 for c in cookies: cookie = c.value - return cookie \ No newline at end of file + return cookie diff --git a/routing.py b/routing.py index 29396db..fd54ebb 100644 --- a/routing.py +++ b/routing.py @@ -18,12 +18,20 @@ from init import * @app.route("/") def index(): + """ + Leitet den normalen Website-Aufruf zum Login weiter. + :return HTML: + """ return redirect(url_for("login")) @app.route("/welcome") @login_required def welcome(): + """ + Interim Homepage + :return HTML: + """ kurs = current_user.kurs name = current_user.name return render_template('index.html', headermessage='DualHub', message="Hallo, " @@ -33,17 +41,29 @@ def welcome(): @app.route("/backendpoc/noten") @login_required def displayNoten(): + """ + Zeigt die Noten aus Dualis an. Hierfür ist ein aktives Token nötig. + :return HTML: + """ d = Dualis.query.filter_by(uid=current_user.id).first() t = d.token sem = d.result_list c = request.cookies.get("cnsc") + timeout = fetchDUALIS.timeOut(d, c, "displayNoten") + if timeout: + return timeout res = fetchDUALIS.getResults(t, c, sem) - return render_template("noten.html", noten=res, semester=fetchDUALIS.getSem(t, c)) + return render_template("noten.html", noten=res, semester=fetchDUALIS.getSem(t, c), sel=sem) @app.route("/backendpoc/plan", methods=["GET"]) @login_required def displayRapla(): + """ + Zeigt den Stundenplan für eingeloggte User an. \n + TODO: Persönliche Filter, Notizen, Essensvorlieben etc. berücksichtigen. + :return HTML: + """ week = request.args.get("week") if week: week = datetime.datetime.strptime(week, "%Y-%m-%d") @@ -58,6 +78,12 @@ def displayRapla(): @app.route("/backendpoc/plan/") def displayPlan(kurs): + """ + Zeigt den Stundenplan ohne Login an. \n + Präferenzen werden nicht berücksichtigt. + :param kurs: + :return HTML: + """ week = request.args.get("week") if week: week = datetime.datetime.strptime(week, "%Y-%m-%d") @@ -81,17 +107,29 @@ def displayPlan(kurs): @app.route("/backendpoc/set-up") def redKurs(): + """ + Setup beginnt mit Kurs. + :return HTML: + """ return redirect(url_for("getKurs")) @app.route("/backendpoc/set-up/kurs") @login_required def getKurs(): + """ + Automatische Kurs-Auswahl. \n + Aktives Dualis-Token benötigt. + :return HTML: + """ d = Dualis.query.filter_by(uid=current_user.id).first() if d: + cookie = request.cookies.get("cnsc") + timeout = fetchDUALIS.timeOut(d, cookie, "getKurs") + if timeout: + return timeout e = False if not current_user.kurs: - cookie = request.cookies.get("cnsc") kurs = fetchDUALIS.getKurs(d.token, cookie) if kurs != 0: current_user.kurs = kurs @@ -111,6 +149,10 @@ def getKurs(): @app.route("/backendpoc/set-up/semester") @login_required def getSemester(): + """ + Manuelle Semester-Auswahl. + :return HTML: + """ t = Dualis.query.filter_by(uid=current_user.id).first().token c = request.cookies.get("cnsc") @@ -120,6 +162,10 @@ def getSemester(): @app.route("/backendpoc/set-up/semester", methods=["POST"]) @login_required def setSemester(): + """ + Speichern der Semester-Auswahl. + :return HTML: + """ n = request.args.get("next") if not n: n = url_for("welcome") @@ -132,13 +178,21 @@ def setSemester(): @app.route("/backendpoc/set-up/rapla") @login_required def chooseRaplas(): + """ + Manuelle Rapla-Auswahl. + :return HTML: + """ r = getRaplas() return render_template("rapla.html", raplas=r) -@login_required @app.route("/backendpoc/set-up/rapla", methods=["POST"]) +@login_required def getRapla(): + """ + Verarbeitet die Eingabe von chooseRaplas(). + :return HTML: + """ file = str(request.form.get("file")) url = str(request.form.get("url")) if file == url == "None": @@ -157,14 +211,22 @@ def getRapla(): @app.route("/backendpoc/log-in") -def login(code: int = None): - if code: - print(code) +def login(): + """ + Login-Maske. + :return HTML: + """ return render_template("login.html") @app.route("/backendpoc/log-in", methods=["POST"]) def login_post(): + """ + Verarbeitet die Eingabe von login(). \n + Falls der User schon angelegt ist, wird das Passwort verglichen. \n + Falls nicht, wird ein neuer angelegt. + :return HTML: + """ email = request.form.get("email") password = request.form.get("password") n = request.args.get("next") @@ -178,7 +240,8 @@ def login_post(): if user: dualis = Dualis.query.filter_by(uid=user.id).first() if check_password_hash(user.password, password): - if not dualis.token or not fetchDUALIS.checkLifetime(dualis.token_created): + cookie = request.cookies.get("cnsc") + if not dualis.token or not fetchDUALIS.checkLifetime(dualis.token_created) or not cookie: new_token = fetchDUALIS.checkUser(email, password) dualis.token = new_token[0] newcookie = requesthelpers.getCookie(new_token[1].cookies) @@ -227,13 +290,17 @@ def login_post(): @app.route("/backendpoc/log-out") @login_required def logout(): + """ + Loggt den User aus. + :return Empty Token: + """ cookie = request.cookies.get("cnsc") dualis = Dualis.query.filter_by(uid=current_user.id).first() fetchDUALIS.logOut(dualis.token, cookie) dualis.token = None db.session.commit() logout_user() - red = make_response(redirect(url_for("login", code=1))) + red = make_response(redirect(url_for("login", code=1, next=url_for("welcome")))) red.set_cookie("cnsc", value="Logged out! Your temporary token " "on our server and the cookie on your device have been deleted.", httponly=True, secure=True) @@ -242,6 +309,12 @@ def logout(): @app.route("/backendpoc/error") def error(ecode): + """ + Error Page für custom-Errors. \n + TODO: Funktion depreciaten. Ersetzen durch Errors auf den entsprechenden Seiten. + :param ecode: + :return: + """ if ecode == 900: msg = "Ungültige RAPLA-URL! Sicher, dass der Link zum DHBW-Rapla führt?" elif ecode == 899: @@ -253,6 +326,9 @@ def error(ecode): @app.errorhandler(HTTPException) def handle(e): + """" + HTTP-Exception-Handler + """ return render_template('index.html', message=e, headermessage="DualHub") diff --git a/templates/noten.html b/templates/noten.html index c0c95c7..800ab8f 100644 --- a/templates/noten.html +++ b/templates/noten.html @@ -13,9 +13,8 @@
diff --git a/templates/plan.html b/templates/plan.html index 5052bdf..e55f904 100644 --- a/templates/plan.html +++ b/templates/plan.html @@ -3,7 +3,7 @@ Vorlesungsplan - +
diff --git a/uwsgi.ini b/uwsgi.ini index 6664f70..aba8eb6 100644 --- a/uwsgi.ini +++ b/uwsgi.ini @@ -1,10 +1,10 @@ [uwsgi] -mount = /dualhub=app:app +mount = /dualhub=routing:app manage-script-name = true pidfile = dualhub_flask.pid master = true processes = 1 -http-socket = :2025 +http-socket = :2024 chmod-socket = 660 vacuum = true enable-threads = true