2 Commits
main ... turbo

Author SHA1 Message Date
8021fdfaf8 Merge branch 'refs/heads/main' into turbo
# Conflicts:
#	init.py
2024-04-10 13:53:51 +02:00
8be441bab9 init turbo 2024-04-05 11:22:14 +02:00
40 changed files with 767 additions and 1185 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ set/
VIRTUAL_ENV/ VIRTUAL_ENV/
calendars/ calendars/
.idea/ .idea/
calendars/list.json

View File

@ -1,33 +0,0 @@
# DualHub
### Der duale Studienplaner!
...
## INSTALLATION
### Voraussetzungen
Natürlich: Eine IDE und Python 3.x
### 1. Dieses Repo klonen
``` shell
git clone https://git.paulmartin.cloud/SoftwareEngineering/DualHub.git
```
(oder per Version Control deiner IDE klonen)
### 2. Die nötigen Pakete installieren
``` shell
pip install -r requirements.txt
pip install "flask[async]"
```
Achtung: Falls deine IDE ein VENV erstellt, zuerst:
``` shell
venv\Scripts\activate
```
### 3. Datenbank erstellen
#### 3.1. [MySQL installieren](https://dev.mysql.com/doc/mysql-installation-excerpt/8.0/en/)
Benutzername und Passwort für DualHub können frei gewählt werden
#### 3.2. Datenbank erstellen
``` mysql
CREATE DATABASE paulmrtn_DUALHUB;
```
Achtung: Sicherstellen, dass der DualHub-User Schreib- und Leseberechtigungen hat!
#### 3.3. Zugangsdaten in ``` getMySQL.py``` eintragen (nur Windows)

View File

@ -6,7 +6,6 @@ import datetime
import recurring_ical_events import recurring_ical_events
from fetchMENSA import getMeals from fetchMENSA import getMeals
import pytz import pytz
from init import *
shortnames = ["mon", "tue", "wed", "thu", "fri", "sat"] shortnames = ["mon", "tue", "wed", "thu", "fri", "sat"]
longnames = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"] longnames = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]
@ -21,7 +20,7 @@ async def getWeek(weekstart: datetime, file: str, showsat: bool):
:param weekstart: :param weekstart:
:param file: :param file:
:param showsat: :param showsat:
:return (Event-Liste, Essens-Liste, voheriges Wochendatum, nächstes Wochendatum, Datum des Montags): :return:
""" """
if weekstart == "today": if weekstart == "today":
start_date = datetime.date.today() start_date = datetime.date.today()
@ -41,10 +40,8 @@ async def getWeek(weekstart: datetime, file: str, showsat: bool):
mon += " " + str(end_date.year) mon += " " + str(end_date.year)
with open("calendars/" + file) as f: with open("calendars/" + file) as f:
calendar = icalendar.Calendar.from_ical(f.read()) calendar = icalendar.Calendar.from_ical(f.read())
events = recurring_ical_events.of(calendar).between(start_date, end_date) events = recurring_ical_events.of(calendar).between(start_date, end_date)
eventl = [] eventl = []
for event in events: for event in events:
estart = event["DTSTART"].dt estart = event["DTSTART"].dt
if str(estart.tzinfo) != "Europe/Berlin": if str(estart.tzinfo) != "Europe/Berlin":
@ -63,13 +60,6 @@ async def getWeek(weekstart: datetime, file: str, showsat: bool):
forml[i] = "0" + forml[i] forml[i] = "0" + forml[i]
formstart = forml[0] formstart = forml[0]
formend = forml[1] formend = forml[1]
try:
if type(event["ATTENDEE"]) is not list:
teacher = [event["ATTENDEE"].params["CN"]]
else:
teacher = [i.params["CN"] for i in event["ATTENDEE"]]
except KeyError:
teacher = ""
eventdict = { eventdict = {
"start": formstart, "start": formstart,
"end": formend, "end": formend,
@ -77,40 +67,18 @@ async def getWeek(weekstart: datetime, file: str, showsat: bool):
"name": event["SUMMARY"], "name": event["SUMMARY"],
"room": event["LOCATION"], "room": event["LOCATION"],
"weekday": estart.weekday(), "weekday": estart.weekday(),
"day": estart.day, "day": estart.day
"teacher": teacher,
"id": event["UID"]
} }
eventl += [eventdict] eventl += [eventdict]
return eventl, await MensaDayList(start_date, showsat), prevw, nextw, mon return eventl, await daylist(start_date, showsat), prevw, nextw, mon
async def createCustomCalendar (uid: int): async def daylist(weekstart: datetime, showsat: bool):
hiddenEvents = HiddenVL.query.filter_by(uid=uid).all()
hiddenEventIDs = {event.eventid for event in hiddenEvents}
kurs = User.query.filter_by(id=uid).first().kurs
raplaFile = Rapla.query.filter_by(name=kurs).first().file
with open(f"calendars/{raplaFile}") as f:
calendar = icalendar.Calendar.from_ical(f.read())
newCalendar = icalendar.Calendar()
for component in calendar.walk():
if component.name != "VEVENT":
if component.name != "VCALENDAR":
newCalendar.add_component(component)
elif component["UID"] not in hiddenEventIDs:
newCalendar.add_component(component)
byteText = newCalendar.to_ical()
with open(f"calendars/{uid}.ical", 'w+') as f:
f.write(byteText.decode("utf-8"))
return uid
async def MensaDayList(weekstart: datetime, showsat: bool):
""" """
Gibt die Essen einer Woche zurück. Gibt die Essen einer Woche zurück.
:param weekstart: :param weekstart:
:param showsat: :param showsat:
:return Essens-Liste: :return:
""" """
weekday = weekstart weekday = weekstart
dayl = [] dayl = []

2
calendars/list.json Normal file
View File

@ -0,0 +1,2 @@
{
}

View File

@ -1 +1 @@
18291 25454

View File

@ -22,15 +22,15 @@ async def checkUser(email: str, password: str):
Erhält von Dualis den Token und Cookie für User. Erhält von Dualis den Token und Cookie für User.
:param email: :param email:
:param password: :param password:
:return (Token, Cookie): :return (Token, Session):
""" """
async with httpx.AsyncClient() as s: async with httpx.AsyncClient() as s:
formattedPassword = urllib.parse.quote(password, safe='', encoding=None, errors=None) fpw = urllib.parse.quote(password, safe='', encoding=None, errors=None)
formattedEmail = urllib.parse.quote(email, safe='', encoding=None, errors=None) fmail = urllib.parse.quote(email, safe='', encoding=None, errors=None)
content = (f"usrname={formattedEmail}&pass={formattedPassword}&ARGUMENTS=clino%2Cusrname%2Cpass%2C" content = (f'usrname={fmail}&pass={fpw}&ARGUMENTS=clino%2Cusrname%2Cpass%2Cmenuno%2Cmenu_type%2Cbrowser'
f"menuno%2Cmenu_type%2Cbrowser%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK") f'%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK')
response = await s.post(url=url, headers=headers, content=content) response = await s.post(url=url, headers=headers, content=content)
header = response.headers header = response.headers
try: try:
refresh = header["REFRESH"] refresh = header["REFRESH"]
arg = refresh.find("=-N") + 3 arg = refresh.find("=-N") + 3
@ -48,19 +48,16 @@ async def getKurs(token: int, cookie: str):
TODO: Umstellen auf Bezeichner INKL. Standort TODO: Umstellen auf Bezeichner INKL. Standort
:param token: :param token:
:param cookie: :param cookie:
:return Kurs-Bezeichner ODER 0 bei Fehler: :return Kurs-Bezeichner:
""" """
try: try:
headers["Cookie"] = "cnsc=" + cookie headers["Cookie"] = "cnsc=" + cookie
token = str(token) token = str(token)
async with httpx.AsyncClient() as s: async with httpx.AsyncClient as s:
response = await s.get(url=f"{url}?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N{token},-N000307,", response = await s.get(url=f"{url}?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N{token},-N000307,",
headers=headers) headers=headers)
html = BeautifulSoup(response.text, 'lxml') html = BeautifulSoup(response.text, 'lxml')
try: link = html.body.find('a', attrs={'id': "Popup_details0001"})['href']
link = html.body.find('a', attrs={'id': "Popup_details0001"})['href']
except TypeError:
return 0
response = await s.get(url=f"{url}{link[21:]}", headers=headers) response = await s.get(url=f"{url}{link[21:]}", headers=headers)
html = BeautifulSoup(response.text, 'lxml') html = BeautifulSoup(response.text, 'lxml')
content = html.body.find('td', attrs={'class': 'level02'}).text content = html.body.find('td', attrs={'class': 'level02'}).text
@ -100,9 +97,9 @@ async def getSem(token: int, cookie: str):
select = select.find_all(value=True) select = select.find_all(value=True)
optlist = [] optlist = []
for i in select: for i in select:
text = i.text.replace("Wi", "Winter").replace("So", "Sommer") t = i.text.replace("Wi", "Winter").replace("So", "Sommer")
text = text.replace("Se", "semester") t = t.replace("Se", "semester")
optlist += [[text, i['value']]] optlist += [[t, i['value']]]
return optlist return optlist
@ -122,25 +119,25 @@ async def getResults(token, cookie: str, resl: str):
html = BeautifulSoup(response.content.decode("utf-8"), 'lxml') html = BeautifulSoup(response.content.decode("utf-8"), 'lxml')
table = html.find('table', attrs={"class": "nb list"}) table = html.find('table', attrs={"class": "nb list"})
body = table.find("tbody") body = table.find("tbody")
vorlesungen = body.find_all("tr") vorl = body.find_all("tr")
vorlesungenList = [] vorlist = []
tasks = [] tasks = []
i = 0 i = 0
for row in vorlesungen: for row in vorl:
columns = row.find_all("td") cols = row.find_all("td")
column = [[e.text.strip()] for e in columns] col = [[e.text.strip()] for e in cols]
if len(column) != 0: if len(col) != 0:
if len(column[4][0]) == 0 or len(column[2][0]) == 0: if len(col[4][0]) == 0:
tasks += [getPruefung(s, row.find("a")["href"])] tasks += [getPruefung(s, row.find("a")["href"])]
column[2] = i col[2] = i
i += 1 i += 1
vorlesungenList += [column[1:4]] vorlist += [col[1:4]]
notlisted = await asyncio.gather(*tasks, return_exceptions=True) notlisted = await asyncio.gather(*tasks, return_exceptions=True)
for i in vorlesungenList: for i in vorlist:
for e in range(0, len(i)): for e in range(0, len(i)):
if isinstance(i[e], int): if isinstance(i[e], int):
i[e] = notlisted[i[e]] i[e] = notlisted[i[e]]
return vorlesungenList[:-1] return vorlist[:-1]
async def getPruefung(s, url): async def getPruefung(s, url):
@ -149,24 +146,24 @@ async def getPruefung(s, url):
TODO: Namen der spezifischen Prüfungen auch zurückgeben, um Zusammensetzung zu spezifizieren. TODO: Namen der spezifischen Prüfungen auch zurückgeben, um Zusammensetzung zu spezifizieren.
:param s: :param s:
:param url: :param url:
:return Noten-Liste: :return list:
""" """
response = await s.get("https://dualis.dhbw.de" + url, headers=headers) response = await s.get("https://dualis.dhbw.de" + url, headers=headers)
html = BeautifulSoup(response.content.decode("utf-8"), 'lxml') html = BeautifulSoup(response.content.decode("utf-8"), 'lxml')
table = html.find('table') table = html.find('table')
pruefung = table.find_all("tr") pruefung = table.find_all("tr")
returnList = [] ret = []
for row in pruefung: for row in pruefung:
columns = row.find_all("td") cols = row.find_all("td")
column = [e.text.strip() for e in columns] col = [e.text.strip() for e in cols]
if len(column) == 6 and len(column[3]) <= 13: if len(col) == 6 and len(col[3]) <= 13:
if len(column[3]) != 0: if len(col[3]) != 0:
returnList += [column[0] + ": " + column[3].split("\xa0")[0]] ret += [col[0] + ": " + col[3][:3]]
if returnList[-1][0] == ':': if ret[-1][0] == ':':
returnList[-1] = "Gesamt" + returnList[-1] ret[-1] = "Gesamt" + ret[-1]
if len(returnList) == 0: if len(ret) == 0:
returnList = ["Noch nicht gesetzt"] ret = ["Noch nicht gesetzt"]
return returnList return ret
def checkLifetime(timecode: float): def checkLifetime(timecode: float):

View File

@ -2,7 +2,7 @@ import json
import asyncio import asyncio
from init import db, Meals, scheduler, flaskApp from init import db, Meals, scheduler, app
import datetime import datetime
import time import time
import httpx import httpx
@ -41,37 +41,36 @@ async def getMealsFromAPI(day: str, dbentry: bool = False):
async with httpx.AsyncClient() as s: async with httpx.AsyncClient() as s:
response = await s.get(url=f"https://dh-api.paulmartin.cloud/plans/{day}?canteens=erzberger") response = await s.get(url=f"https://dh-api.paulmartin.cloud/plans/{day}?canteens=erzberger")
response = response.content response = response.content
jsonResponse = json.loads(response.decode("utf-8")) jres = json.loads(response.decode("utf-8"))
essen = [] essen = []
try: try:
number = len(jsonResponse["data"][0]["lines"]) num = len(jres["data"][0]["lines"])
for i in range(number): for i in range(num):
try: try:
jsonMeals = jsonResponse["data"][0]["lines"][i]["meals"] jsmeal = jres["data"][0]["lines"][i]["meals"]
hasContent = True cont = True
except IndexError: except IndexError:
essen = [] essen = []
hasContent = False cont = False
if hasContent: if cont:
for e in range(len(jsonMeals)): for e in range(len(jsmeal)):
jsonEntry = jsonMeals[e] ji = jsmeal[e]
name = jsonEntry["name"] name = ji["name"]
if pricetofloat(jsonEntry["price"]) >= 1.1: if pricetofloat(ji["price"]) >= 1.1:
vegan = jsonEntry["classifiers"].count("VG") == 1 vegan = ji["classifiers"].count("VG") == 1
schwein = jsonEntry["classifiers"].count("S") == 1 schwein = ji["classifiers"].count("S") == 1
if vegan: if vegan:
vegetarian = True veget = True
else: else:
vegetarian = jsonEntry["classifiers"].count("VEG") == 1 veget = ji["classifiers"].count("VEG") == 1
if vegetarian: if veget:
if name.count("Reibekäse") > 0: if name.count("Reibekäse") > 0:
vegan = True vegan = True
essen += [name] essen += [name]
if dbentry: if dbentry:
mid = int(time.time() * 1000) % 100000 mid = int(time.time() * 1000) % 100000
neu = Meals(date=day, name=name, id=mid, vegan=vegan, vegetarian=vegetarian, neu = Meals(date=day, name=name, id=mid, vegan=vegan, vegetarian=veget, schwein=schwein)
schwein=schwein)
db.session.add(neu) db.session.add(neu)
db.session.commit() db.session.commit()
if not essen: if not essen:
@ -103,20 +102,20 @@ def formatDay(day: datetime):
:return str: :return str:
""" """
if day.month < 10: if day.month < 10:
monat = "0" + str(day.month) mon = "0" + str(day.month)
else: else:
monat = str(day.month) mon = str(day.month)
if day.day < 10: if day.day < 10:
tag = "0" + str(day.day) tag = "0" + str(day.day)
else: else:
tag = str(day.day) tag = str(day.day)
formattedDay = str(day.year) + "-" + monat + "-" + tag day = str(day.year) + "-" + mon + "-" + tag
return formattedDay return day
async def refreshMeals(): async def refreshMeals():
""" """
Aktualisiert alle Mahlzeiten in der Datenbank. \n Aktualisiert immer vormittags alle Mahlzeiten in der Datenbank. \n
Datenbankeinträge werden ersetzt, wenn die API andere Mahlzeiten liefert. Datenbankeinträge werden ersetzt, wenn die API andere Mahlzeiten liefert.
""" """
print("Aktualisiere Essenspläne...\n") print("Aktualisiere Essenspläne...\n")
@ -129,23 +128,20 @@ async def refreshMeals():
for i in range(len(dates)): for i in range(len(dates)):
dates[i] = formatDay(dates[i]) dates[i] = formatDay(dates[i])
for i in dates: for i in dates:
apiNames = await getMealsFromAPI(i) apinames = await getMealsFromAPI(i)
dbMeals = Meals.query.filter_by(date=i).all() apinames = await getMealsFromAPI(i)
dbNames = [] dbmeals = Meals.query.filter_by(date=i).all()
for meal in dbMeals: dbnames = []
dbNames += [meal.name] for m in dbmeals:
if set(dbNames) != set(apiNames) and nomeal not in apiNames: dbnames += [m.name]
for name in dbNames: if set(dbnames) != set(apinames) and nomeal not in apinames:
db.session.delete(Meals.query.filter_by(date=i, name=name).first()) for n in dbnames:
db.session.delete(Meals.query.filter_by(date=i, name=n).first())
db.session.commit() db.session.commit()
await getMealsFromAPI(i, True) await getMealsFromAPI(i, True)
@scheduler.task('cron', id="mensaSchedule", hour='8-11', day_of_week='*', minute='*/15', week='*', second='5') @scheduler.task('cron', id="mensaschedule", hour='8-11', day_of_week='*', minute='*/15', week='*', second='5')
def mensaSchedule(): def mensaschedule():
""" with app.app_context():
Nutzt vormittags die Funktion refreshMeals(), um die Essen zu aktualisieren asyncio.run(refreshMeals())
"""
if not flaskApp.config["TESTING"]:
with flaskApp.app_context():
asyncio.run(refreshMeals())

View File

@ -1,60 +1,33 @@
import urllib.error import urllib.error
from datetime import datetime, timedelta from urllib.request import urlretrieve
from dateutil.parser import *
import asyncio import asyncio
import httpx import httpx
import icalendar import icalendar
from dateutil.relativedelta import relativedelta
from icalendar import Calendar, Event
import json import json
import recurring_ical_events
from init import scheduler, app
from init import scheduler, flaskApp, Rapla, db, HiddenVL, User
from calendar_generation import createCustomCalendar
async def fetchPlan(session, url): async def fetchPlan(session, url):
"""
Hilfsfunktion, liefert die Response auf einen GET-Request zur angegebenen URL
:param session:
:param url:
:return Response:
"""
return await session.get(url=url) return await session.get(url=url)
def writeToFile(filename, data): def writeToFile(filename, data):
""" with open(filename, 'w+') as f:
Schreibt die Daten in die angegebene Datei. Erstellt die Datei, falls sie noch nicht existiert. f.write(data.text)
:param filename: f.close()
:param data:
"""
with open(filename, 'w+') as file:
assert "BEGIN:VCALENDAR" in data.text
file.write(data.text)
file.close()
def writeKursToDB(kurs, url): def parseURL(url: str):
""" """
Schreibt Kurs und URL in die Datenbank Konvertiert URLs ins korrekte Format. \n
:param kurs:
:param url:
"""
if Rapla.query.filter_by(name=kurs).first() is None:
new_kurs = Rapla(name=kurs, link=url, file=f"rapla{kurs}.ical")
db.session.add(new_kurs)
db.session.commit()
def parseRaplaURL(url: str):
"""
Konvertiert Rapla-URLs ins korrekte Format. \n
Konvertiert werden: http, www.; page=calendar \n Konvertiert werden: http, www.; page=calendar \n
In: https; page=ical In: https; page=ical
:param url: :param url:
:return [Status: int, URL: str]: :return str:
""" """
rapla = url.find("rapla.") rapla = url.find("rapla.")
if rapla == -1: if rapla == -1:
@ -63,197 +36,97 @@ def parseRaplaURL(url: str):
url = url[rapla:] url = url[rapla:]
http = url.find(":") http = url.find(":")
if url[:http] == "http": if url[:http] == "http":
url = f"https{url[http:]}" url = "https" + url[http:]
elif http == -1: elif http == -1:
url = f"https://{url}" url = "https://" + url
pageInURL = url.find("page=") p = url.find("page=")
andInURL = url.find("&") u = url.find("&")
if (url[pageInURL + 5:andInURL]).lower() == "ical": if (url[p + 5:u]).lower() == "ical":
return 1, url return url
elif pageInURL != -1: elif p != -1:
return 1, f"{url[:pageInURL + 5]}ical{url[andInURL:]}" return url[:p + 5] + "ical" + url[u:]
elif url.find("key") != -1:
return 2, url
else: else:
return 0, 0 return 0
async def getNewRapla(url: str, testing=False): async def getNewRapla(url: str):
""" """
Speichert den iCal eines Raplas auf dem Server. \n Speichert den iCal eines Raplas auf dem Server. \n
Gibt Namen der Datei zurück. \n Gibt Namen der Datei zurück. \n
TODO: Standort zu Dateibezeichner hinzufügen, um Konflikte zu vermeiden. TODO: Standort zu Dateibezeichner hinzufügen, um Konflikte zu vermeiden.
:param url: :param url:
:param opt. testing:
:return str: :return str:
""" """
parsed = parseRaplaURL(url) url = parseURL(url)
if url == 0:
if testing:
assert "https" in parsed[1]
try:
assert "ical" in parsed[1]
except AssertionError:
assert "key" in parsed[1]
if parsed[0] == 0:
return 0 return 0
elif parsed[0] == 1: urlfile = url.find("file=")
url = parsed[1] kurs = url[urlfile + 5:].upper()
elif parsed[0] == 2:
return await buildICALfromKey(parsed[1], onlyUpdate=False, testing=testing) try:
fileInURL = url.find("file=") urlretrieve(url, "calendars/rapla" + kurs + ".ical")
kurs = url[fileInURL + 5:].replace(" ", "").upper() async with httpx.AsyncClient() as s:
if url[-5:] != ".ical": response = await fetchPlan(s, url)
try: writeToFile(f"calendars/rapla{kurs}.ical", response)
async with httpx.AsyncClient() as s: except urllib.error.URLError:
response = await fetchPlan(s, url) return -1
if testing: file = open("calendars/list.json", "r+")
assert "Vollmer" in response.text jsoncal = json.load(file)
writeToFile(f"calendars/rapla{kurs}.ical", response) jsoncal.update({kurs: [f"rapla{kurs}.ical", url]})
except urllib.error.URLError: file.close()
return -1 file = open("calendars/list.json", "w")
writeKursToDB(kurs, url) json.dump(jsoncal, file, indent=4)
if testing: return f"rapla{kurs}.ical"
assert "TINF22B3" in Rapla.query.filter(Rapla.name == "TINF22B3").first().file
return f"rapla{kurs}.ical"
else:
return url
def getIcal(uid: int | None = None, kurs: str | None = None): def getIcal(kurs: str):
""" """
Liefert den Namen der Datei des mitgegebenen Users. Liefert den Namen der Datei des mitgegebenen Kurses.
:param uid:
:param kurs: :param kurs:
:return str: :return str:
""" """
if uid: file = open("calendars/list.json", "r")
if HiddenVL.query.filter_by(uid=uid).first(): jf = json.load(file)
return f"{uid}.ical"
else:
kurs = User.query.filter_by(id=uid).first().kurs
rapla = Rapla.query.filter(Rapla.name == kurs).first()
try: try:
return rapla.file return jf[kurs][0]
except AttributeError or KeyError: except KeyError:
return None return None
def getRaplas(): def getRaplas():
""" """
Liefert alle in der Datenbank gespeicherten Raplas. Liefert alle auf dem Server gespeicherten Raplas.
:return (Kursliste, Dateiliste, URL-Liste): :return (Kursliste, Dateiliste, URL-Liste):
""" """
raplas = Rapla.query.all() file = open("calendars/list.json", "r")
kursList = [rapla.name for rapla in raplas] jsonf = json.load(file)
fileList = [rapla.file for rapla in raplas] kursl = []
urlList = [rapla.link for rapla in raplas] filel = []
return kursList, fileList, urlList urll = []
for i in jsonf:
kursl += [i]
filel += [jsonf[i][0]]
urll += [jsonf[i][1]]
return sorted(kursl), sorted(filel), sorted(urll)
async def refreshRapla(): async def refreshRapla():
""" """
Aktualisiert alle gespeicherten Raplas. Aktualisiert alle 5 Minuten alle gespeicherten Raplas.
""" """
fileList = getRaplas()[1] filel = getRaplas()[1]
urlList = getRaplas()[2] urll = getRaplas()[2]
jobList = [] jobl = []
async with httpx.AsyncClient() as s: async with httpx.AsyncClient() as s:
for i in range(len(fileList)): for i in range(len(filel)):
print(f"Update Rapla: {fileList[i][:-5]}") print(f"Update Rapla: {filel[i][:-5]}")
if urlList[i].find("file") != -1: jobl += [fetchPlan(s, urll[i])]
jobList += [fetchPlan(s, urlList[i])] callist = await asyncio.gather(*jobl, return_exceptions=True)
else: for cal in range(len(callist)):
jobList += [buildICALfromKey(urlList[i], onlyUpdate=True)] writeToFile(f"calendars/{filel[cal]}", callist[cal])
calendarList = await asyncio.gather(*jobList, return_exceptions=True)
for calendar in range(len(calendarList)):
if calendarList[calendar] != 200:
writeToFile(f"calendars/{fileList[calendar]}", calendarList[calendar])
hiddenEvents = HiddenVL.query.all()
hiddenUsers = {event.uid for event in hiddenEvents}
for user in hiddenUsers:
uid = await createCustomCalendar(user)
print (f"Update custom calendar: {uid}")
@scheduler.task('cron', id="raplaSchedule", hour='*', day_of_week='*', minute='*/3', week='*', second='40') @scheduler.task('cron', id="raplaschedule", hour='*', day_of_week='*', minute='*/15', week='*')
def raplaSchedule(): def raplaschedule():
""" with app.app_context():
Nutzt alle 3 Minuten refreshRapla() um die Stundenpläne zu aktualisieren. asyncio.run(refreshRapla())
"""
if not flaskApp.config["TESTING"]:
with flaskApp.app_context():
asyncio.run(refreshRapla())
async def buildICALfromKey(url, onlyUpdate, testing=False):
"""
Baut eine .ical-Datei aus der mitgegebenen URL, die ein Key-Parameter enthalten muss.
:param url:
:param onlyUpdate:
:param opt. testing:
:return Dateinamen:
"""
async with httpx.AsyncClient() as s:
page = await s.get(url=url)
if page.text[:10] == "BEGIN:VCAL":
info = page.headers['content-disposition']
kursname = info[info.find('filename=')+9:-4].upper()
if testing:
assert "TMB22" in kursname
writeToFile(f"calendars/rapla{kursname}.ical", page)
if not onlyUpdate:
writeKursToDB(kursname, url)
assert "TMB22" in Rapla.query.filter(Rapla.name == kursname).first().file
return f"rapla{kursname}.ical"
else:
return 200
else:
kursname = page.text[page.text.find("<title>") + 7:page.text.find("</title>")].replace(" ", "")
if testing:
assert "TMT22B1" in kursname
if len(kursname) > 15:
return 0
start = f'{(datetime.now() - relativedelta(days=100)):%Y-%m-%d}'
end = f'{(datetime.now() + relativedelta(days=100)):%Y-%m-%d}'
payload = {"url": url, "start": start, "end": end}
if testing:
req = await s.post(url="https://dh-api.paulmartin.cloud/rapla", data=payload,
headers={'Content-Type': 'application/x-www-form-urlencoded'}, timeout=20)
else:
req = await s.post(url="https://dh-api.paulmartin.cloud/rapla", data=payload,
headers={'Content-Type': 'application/x-www-form-urlencoded'}, timeout=10)
jsonresp = json.loads(req.text)
if testing:
assert len(jsonresp) > 0
cal = Calendar()
cal.add('prodid', '-//Rapla//iCal Plugin//EN')
cal.add('version', '2.0')
for i in jsonresp:
if len(jsonresp[i]) != 0:
for jsonday in jsonresp[i]:
event = Event()
event.add('SUMMARY', jsonday['title'])
event.add('LOCATION', jsonday['ressources'])
event.add('DTSTART', parse(jsonday['startDate'], dayfirst=True))
event.add('DTEND', parse(jsonday['endDate'], dayfirst=True))
event.add('UID', jsonday['UID'])
try:
teacher = icalendar.vCalAddress(value=jsonday['persons'])
teacher.params["CN"] = jsonday['persons']
except KeyError:
teacher = icalendar.vCalAddress(value="")
teacher.params["CN"] = ""
event.add('ATTENDEE', teacher)
cal.add_component(event)
with open(f"calendars/rapla{kursname}.ical", "wb") as f:
f.write(cal.to_ical())
f.close()
if not onlyUpdate:
writeKursToDB(kursname, url)
if testing:
assert "TMT22B1" in Rapla.query.filter(Rapla.name == "TMT22B1").first().file
return f"rapla{kursname}.ical"
else:
return 200

View File

@ -1,42 +0,0 @@
"""
Generiert das SCSS für cal.scss
"""
css = ""
for startEnd in ("start", "end"):
acht = 0
for i in range(0, 28):
if i % 4 == 0:
acht += 100
height = str(acht)
else:
height = str(acht + (i % 4 * 15))
if len(height) == 3:
height = "0" + height
css += "." + startEnd + "-" + height + " {\n"
if startEnd == "start":
css += "grid-row-" + startEnd + ": " + "1" + "\n}\n"
else:
css += "grid-row-" + startEnd + ": " + "4" + "\n}\n"
css += "\n\n\n"
for startEnd in ("start", "end"):
acht = 700
for i in range(0, 45):
if i % 4 == 0:
acht += 100
height = str(acht)
else:
height = str(acht + (i % 4 * 15))
if len(height) == 3:
height = "0" + height
css += "." + startEnd + "-" + height + " {\n"
css += "grid-row-" + startEnd + ": " + str(i + 1) + "\n}\n"
css += "\n\n\n"
file = open("static/cal.scss", "a")
file.write ("\n // Generated by genSCSSstarts.py\n\n")
file.write(css)
file.close()

40
genstarts.py Normal file
View File

@ -0,0 +1,40 @@
from flask import url_for
css = ""
for se in ("start", "end"):
acht = 0
for i in range(0, 28):
if i % 4 == 0:
acht += 100
h = str(acht)
else:
h = str(acht + (i % 4 * 15))
if len(h) == 3:
h = "0" + h
css += "." + se + "-" + h + " {\n"
if se == "start":
css += "grid-row-" + se + ": " + "1" + "\n}\n"
else:
css += "grid-row-" + se + ": " + "4" + "\n}\n"
css += "\n\n\n"
for se in ("start", "end"):
acht = 700
for i in range(0, 45):
if i % 4 == 0:
acht += 100
h = str(acht)
else:
h = str(acht + (i % 4 * 15))
if len(h) == 3:
h = "0" + h
css += "." + se + "-" + h + " {\n"
css += "grid-row-" + se + ": " + str(i+1) + "\n}\n"
css += "\n\n\n"
f = open("static/cal.scss", "a")
f.write ("\n // Generated by genstarts.py\n\n")
f.write(css)
f.close()

View File

@ -1,23 +0,0 @@
import getpass
import sys
# noinspection PyPackageRequirements
def getMySQL():
"""
Extrahiert die MySQL-Anmeldedaten aus ~/.my.cnf . \n
Funktioniert wahrscheinlich nur auf Linux, vor allem für den Server gedacht.
:return: [username: str, password: str]:
"""
if sys.platform == "linux":
systemUser = getpass.getuser()
file = open("/home/"+systemUser+"/.my.cnf", "r")
content = file.read()
username = content.find("user=")
password = content.find("password=")
username = content[username+5:password-1]
readOnly = content.find("[clientreadonly]")
password = content[password+9:readOnly-2]
return username, password
else:
return "username-goes-here", "password-goes-here"

17
get_mysql.py Normal file
View File

@ -0,0 +1,17 @@
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()
u = i.find("user=")
p = i.find("password=")
u = i[u+5:p-1]
ro = i.find("[clientreadonly]")
p = i[p+9:ro-2]
return u, p

6
init-sql.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
#Erstellt alle benötigten MySQL-Tabellen.
mysql -e "USE paulmrtn_DUALHUB; CREATE TABLE user ( id int NOT NULL, email 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));"
mysql -e "USE paulmrtn_DUALHUB; CREATE TABLE meals ( id int NOT NULL, date date, name VARCHAR(200), vegetarian tinyint(1), vegan tinyint(1), schwein tinyint(1), PRIMARY KEY (id));"

63
init.py
View File

@ -1,28 +1,24 @@
import apscheduler
from flask import Flask from flask import Flask
from flask_login import LoginManager, UserMixin from flask_login import LoginManager, UserMixin
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_talisman import Talisman from flask_talisman import Talisman
from sqlalchemy import ForeignKey
import sys
from getMySQL import getMySQL from get_mysql import get_mysql
import atexit import atexit
from flask_apscheduler import APScheduler from flask_apscheduler import APScheduler
def create(testing: bool = False): def create():
""" """
Erstellt die Flask-App inkl. Datenbank und Login-Manager. Erstellt die Flask-App inkl. Datenbank und Login-Manager.
:return app: :return app:
""" """
app = Flask(__name__) app = Flask(__name__)
dbpw = getMySQL()[1] dbpw = get_mysql()[1]
dbun = getMySQL()[0] dbun = get_mysql()[0]
try: app.config['SECRET_KEY'] = 'SECRET_KEY_GOES_HERE'
app.config['SECRET_KEY'] = sys.argv[1] # Den Secret Key bei Start mitgeben: routing.py SECRET_KEY_GOES HERE
except IndexError:
app.config['SECRET_KEY'] = "SECRET-KEY-GOES-HERE"
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://' + dbun + ':' + dbpw + '@localhost/paulmrtn_DUALHUB' app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://' + dbun + ':' + dbpw + '@localhost/paulmrtn_DUALHUB'
db.init_app(app) db.init_app(app)
@ -31,8 +27,7 @@ def create(testing: bool = False):
login_manager.login_view = "login" login_manager.login_view = "login"
# Shut down the scheduler when exiting the app # Shut down the scheduler when exiting the app
if not testing: atexit.register(lambda: scheduler.shutdown())
atexit.register(lambda: scheduler.shutdown())
@login_manager.user_loader @login_manager.user_loader
def load_user(uid: int): def load_user(uid: int):
@ -48,7 +43,6 @@ class User(UserMixin, db.Model):
""" """
Datenbank-Modell für User. Datenbank-Modell für User.
""" """
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True) email = db.Column(db.String(255), unique=True)
name = db.Column(db.String(255)) name = db.Column(db.String(255))
@ -59,9 +53,8 @@ class Dualis(db.Model):
""" """
Datenbank-Modell für Dualis. Datenbank-Modell für Dualis.
""" """
__tablename__ = 'dualis'
token = db.Column(db.String(255), unique=True) token = db.Column(db.String(255), unique=True)
uid = db.Column(db.Integer, ForeignKey('user.id', ondelete='CASCADE'), primary_key=True) uid = db.Column(db.Integer, primary_key=True)
token_created = db.Column(db.Integer) token_created = db.Column(db.Integer)
semester = db.Column(db.String(15)) semester = db.Column(db.String(15))
@ -70,55 +63,29 @@ class Semesterlist(db.Model):
""" """
Datenbank-Modell für Semester-Liste. Datenbank-Modell für Semester-Liste.
""" """
__tablename__ = 'semesterlist' uid = db.Column(db.Integer)
uid = db.Column(db.Integer, ForeignKey('user.id', ondelete='CASCADE'))
semestername = db.Column(db.String(25)) semestername = db.Column(db.String(25))
semesterid = db.Column(db.String(15)) semesterid = db.Column(db.String(15))
itemid = db.Column(db.Integer, primary_key=True) itemid = db.Column(db.Integer, primary_key=True)
class Rapla(db.Model):
"""
Datenbank-Modell für Rapla.
"""
__tablename__ = 'rapla'
name = db.Column(db.String(15), primary_key=True)
file = db.Column(db.String(20), unique=True)
link = db.Column(db.String(255), unique=True)
class HiddenVL(db.Model):
"""
Datenbank-Modell für ausgeblendete Vorlesungen.
"""
__tablename__ = 'hiddenVL'
uid = db.Column(db.Integer, ForeignKey('user.id', ondelete='CASCADE'))
eventid = db.Column(db.String(255))
id = db.Column(db.String(255), primary_key=True)
name = db.Column(db.String(255))
class Meals(db.Model): class Meals(db.Model):
""" """
Datenbank-Modell für Meals. Datenbank-Modell für Meals.
""" """
__tablename__ = 'meals'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.Date) date = db.Column(db.Date)
name = db.Column(db.String(200)) name = db.Column(db.String(100))
vegetarian = db.Column(db.Boolean) vegetarian = db.Column(db.Boolean)
vegan = db.Column(db.Boolean) vegan = db.Column(db.Boolean)
schwein = db.Column(db.Boolean) schwein = db.Column(db.Boolean)
scheduler = APScheduler() scheduler = APScheduler ()
flaskApp = create() app = create()
with flaskApp.app_context():
print("Creating Tables....")
db.create_all()
def_src = ["*.paulmartin.cloud", '\'self\''] def_src = ["*.paulmartin.cloud", '\'self\'']
Talisman(flaskApp, content_security_policy={"default-src": def_src, "script-src": def_src # + ["'unsafe-inline'"] Talisman(app, content_security_policy={"default-src": def_src, "script-src": def_src # + ["'unsafe-inline'"]
}) })
scheduler.init_app(flaskApp) scheduler.init_app(app)
scheduler.start() scheduler.start()
scheduler.api_enabled = True scheduler.api_enabled = True

View File

@ -1,13 +1,11 @@
from flask_login import current_user as currentUser from init import Semesterlist, User
import fetchDUALIS
from init import Semesterlist, User, db
from datetime import datetime
def getCookie(cookies): def getCookie(cookies):
""" """
Liefert (letzten) Cookie der Cookies-Liste zurück. Liefert (letzten) Cookie der Cookies-Liste zurück.
:param cookies: :param cookies:
:return Cookie: :return:
""" """
cookie = 0 cookie = 0
for c in cookies: for c in cookies:
@ -15,49 +13,14 @@ def getCookie(cookies):
return cookie return cookie
async def getSemesterList(uid, token, cookie): def getSemesterList(uid):
""" semesterlist = Semesterlist.query.filter_by(uid=uid).all()
Liefert die IDs der Semester für den User semester = []
:param uid: for s in semesterlist:
:param token: semester += [[s.semestername, s.semesterid]]
:param cookie: semester.sort(key=lambda x: x[-1], reverse=True)
:return Semester-ID-Liste: return semester
"""
dbSemesterList = Semesterlist.query.filter_by(uid=uid).all()
semesterList = []
for semester in dbSemesterList:
semesterList += [[semester.semestername, semester.semesterid]]
semesterList.sort(key=lambda x: x[-1], reverse=True)
shortList = (int(x[0][-2:]) for x in semesterList)
for shortYear in shortList:
if (shortYear > datetime.now().year-2000) or (len(semesterList) == 6):
return semesterList
return await semesterDualisToDB(semesterList, token, cookie)
async def semesterDualisToDB (semesterlist, token, cookie):
"""
Gleicht die Semester-Einträge der mitgegebenen Liste mit den Semester-Einträgen von Dualis ab und schreibt
Differenzen in die Datenbank.
:param semesterlist:
:param token:
:param cookie:
:return Semester-ID-Liste:
"""
semesterDualis = await fetchDUALIS.getSem(token, cookie)
for i in semesterDualis:
if i not in semesterlist:
semsterItem = Semesterlist(semestername=i[0], semesterid=i[1], uid=currentUser.id,
itemid=currentUser.id * int(i[1][-7:]) // 1000000)
db.session.add(semsterItem)
db.session.commit()
return semesterDualis
def loadUser(uid): def loadUser(uid):
"""
Hilfsfunktion, die den User für die UID zurückgibt.
:param uid:
:return User:
"""
return User.query.filter_by(id=uid).first() return User.query.filter_by(id=uid).first()

View File

@ -5,20 +5,13 @@ Flask_Login
flask_sqlalchemy flask_sqlalchemy
icalendar~=5.0.11 icalendar~=5.0.11
recurring_ical_events recurring_ical_events
Requests~=2.31.0
talisman talisman
Werkzeug~=3.0.0 Werkzeug~=3.0.0
lxml lxml
bs4~=0.0.1 bs4~=0.0.1
pytz~=2023.3.post1 pytz~=2023.3.post1
flask_talisman
asyncio~=3.4.3 asyncio~=3.4.3
httpx httpx~=1.0.0b0
celery~=5.4.0rc2 celery~=5.4.0rc2
flask[async]
pymysql
APScheduler
cryptography
python-dateutil~=2.9.0.post0
requests~=2.31.0
pytest
pytest-asyncio

View File

@ -1,390 +1,368 @@
#!/usr/bin/env python3.6 #!/usr/bin/env python3.6
from flask import make_response from flask import make_response
from flask import render_template, url_for, redirect, request, send_from_directory from flask import render_template, url_for, redirect, request
from flask_login import (login_user as loginUser, login_required as loginRequired, from flask_login import login_user, login_required, current_user, logout_user
logout_user as logoutUser, current_user as currentUser)
from sqlalchemy.exc import IntegrityError
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from werkzeug.security import generate_password_hash, check_password_hash
import hashlib import hashlib
import datetime import datetime
import time import time
import random
import fetchDUALIS import fetchDUALIS
import fetchMENSA
import fetchRAPLA import fetchRAPLA
from requesthelpers import * from requesthelpers import *
from fetchRAPLA import * from fetchRAPLA import *
from calendar_generation import getWeek, createCustomCalendar from calendar_generation import getWeek
from init import * from init import *
def initRoutes(app: Flask): @app.route("/")
def index():
""" """
Initialisiert die App-Routen. Nötig für Tests. Leitet den normalen Website-Aufruf zum Login weiter.
:param app: :return HTML:
""" """
if current_user.is_authenticated:
return welcome()
else:
return login()
@app.route("/")
def index():
"""
Leitet den normalen Website-Aufruf zum Login weiter.
:return HTML:
"""
return redirect(url_for("login"))
@app.route("/dashboard")
@loginRequired
def welcome():
"""
Dashboard
:return HTML:
"""
if not currentUser.kurs:
return redirect(url_for("getKurs", next=url_for(request.endpoint)))
selectedPhase = request.args.get("sel")
if not selectedPhase:
selectedPhase = "theorie"
kurs = currentUser.kurs
name = currentUser.name
if selectedPhase == "theorie":
theorie = ""
praxis = "hidden"
else:
theorie = "hidden"
praxis = ""
return render_template('dashboard.html', kurs=kurs, name=name, theorie=theorie, praxis=praxis)
@app.route("/theorie/noten", methods=["GET", "POST"]) @app.route("/dashboard", methods=["GET", "POST"])
@loginRequired @login_required
async def displayNoten(): def welcome():
""" """
Zeigt die Noten aus Dualis an. Hierfür ist ein aktives Token nötig. Dashboard
:return HTML: :return HTML:
""" """
dualisUser = Dualis.query.filter_by(uid=currentUser.id).first() if not current_user.kurs:
if request.method == "POST": return redirect(url_for("getKurs", next=url_for(request.endpoint)))
dualisUser.semester = request.form.get("sem") sel = request.args.get("sel")
db.session.commit() if not sel:
if not dualisUser.semester: sel = "theorie"
return redirect(url_for("getSemester", next=url_for(request.endpoint))) kurs = current_user.kurs
token = dualisUser.token name = current_user.name
chosenSemester = dualisUser.semester if sel == "theorie":
cookie = request.cookies.get("cnsc") t = ""
timeout = fetchDUALIS.timeOut(dualisUser, cookie, "displayNoten") p = "hidden"
if timeout: else:
return timeout t = "hidden"
semester = await getSemesterList(currentUser.id, token, cookie) p = ""
noten = await fetchDUALIS.getResults(token, cookie, chosenSemester) if turbo.can_stream():
return render_template("noten.html", noten=noten, semester=semester, sel=chosenSemester, s="n", print ("Streaming...")
praxis="hidden") return turbo.stream([
turbo.update(
render_template('dashboard.html', kurs=kurs, name=name, theorie=t, praxis=p), target="content")
])
else:
print ("Not streaming!")
return render_template('dashboard.html', kurs=kurs, name=name, theorie=t, praxis=p)
@app.route("/plan", methods=["GET"])
@loginRequired @app.route("/theorie/noten", methods=["GET", "POST"])
async def displayRapla(): @login_required
""" async def displayNoten():
Zeigt den Stundenplan für eingeloggte User an. \n """
TODO: Persönliche Filter, Notizen, Essensvorlieben etc. berücksichtigen. Zeigt die Noten aus Dualis an. Hierfür ist ein aktives Token nötig.
:return HTML: :return HTML:
""" """
if not currentUser.kurs: d = Dualis.query.filter_by(uid=current_user.id).first()
return redirect(url_for("getKurs", next=url_for(request.endpoint))) if request.method == "POST":
week = request.args.get("week") d.semester = request.form.get("sem")
if week and week!="today": db.session.commit()
week = datetime.strptime(week, "%Y-%m-%d") if not d.semester:
else: return redirect(url_for("getSemester", next=url_for(request.endpoint)))
week = "today" t = d.token
chosensemester = d.semester
c = request.cookies.get("cnsc")
timeout = fetchDUALIS.timeOut(d, c, "displayNoten")
if timeout:
return timeout
semester = getSemesterList(current_user.id)
noten = await fetchDUALIS.getResults(t, c, chosensemester)
return render_template("noten.html", noten=noten, semester=semester, sel=chosensemester, s="n", praxis="hidden")
@app.route("/plan", methods=["GET"])
@login_required
async def displayRapla():
"""
Zeigt den Stundenplan für eingeloggte User an. \n
TODO: Persönliche Filter, Notizen, Essensvorlieben etc. berücksichtigen.
:return HTML:
"""
if not current_user.kurs:
return redirect(url_for("getKurs", next=url_for(request.endpoint)))
week = request.args.get("week")
if week:
week = datetime.datetime.strptime(week, "%Y-%m-%d")
else:
week = "today"
samstag = request.args.get("samstag")
if not samstag:
samstag = False
events = await getWeek(week, fetchRAPLA.getIcal(current_user.kurs), samstag)
return render_template("plan-user.html", events=events[0], eventdays=events[1],
name=current_user.name, prev=str(events[2])[:10], next=str(events[3])[:10], mon=events[4],
s="p", praxis="hidden")
@app.route("/plan/<string:kurs>")
async 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")
else:
week = "today"
try:
if current_user.kurs == kurs.upper():
return redirect(url_for("displayRapla"))
except AttributeError:
pass
kurs = kurs.upper()
plan = fetchRAPLA.getIcal(kurs)
if plan:
samstag = request.args.get("samstag") samstag = request.args.get("samstag")
if not samstag: if not samstag:
samstag = False samstag = False
events = await getWeek(week, fetchRAPLA.getIcal(uid=currentUser.id), samstag) events = await getWeek(week, plan, samstag)
return render_template("plan-user.html", events=events[0], eventdays=events[1], return render_template("plan-anon.html", events=events[0], eventdays=events[1], kurs=kurs,
uid=currentUser.id, name=currentUser.name, prev=str(events[2])[:10], prev=str(events[2])[:10], next=str(events[3])[:10], mon=events[4], praxis="hidden")
next=str(events[3])[:10], mon=events[4], login=True, s="p", date=str(week)[:10], else:
praxis="hidden") return redirect(url_for("login"))
@app.route("/plan/<string:kurs>")
async def displayPlan(kurs: str):
"""
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.strptime(week, "%Y-%m-%d")
else:
week = "today"
try:
if currentUser.kurs == kurs.upper():
return redirect(url_for("displayRapla"))
except AttributeError:
pass
kurs = kurs.upper()
plan = fetchRAPLA.getIcal(kurs=kurs)
if plan:
samstag = request.args.get("samstag")
if not samstag:
samstag = False
events = await getWeek(week, plan, samstag)
return render_template("plan-anon.html", events=events[0], eventdays=events[1], kurs=kurs,
login=False, prev=str(events[2])[:10], next=str(events[3])[:10],
mon=events[4], praxis="hidden")
else:
return redirect(url_for("login"))
@app.route("/plan/restore") @app.route("/set-up")
@loginRequired def redKurs():
async def restoreVL(): """
events = HiddenVL.query.filter_by(uid=currentUser.id).all() Setup beginnt mit Kurs.
return render_template("restore-events.html", s="s", events=events, praxis="hidden") :return HTML:
"""
return redirect(url_for("getKurs"))
@app.route("/plan/calendar/<int:uid>", methods=["GET"])
async def deliverCalendar(uid: int):
calstring = fetchRAPLA.getIcal(uid=uid)
return send_from_directory("calendars", calstring, as_attachment=False)
@app.route("/set-up") @app.route("/set-up/kurs")
def redKurs(): @login_required
""" async def getKurs():
Setup beginnt mit Kurs. """
:return HTML: Automatische Kurs-Auswahl. \n
""" Aktives Dualis-Token benötigt.
return redirect(url_for("getKurs")) :return HTML:
"""
@app.route("/set-up/kurs") d = Dualis.query.filter_by(uid=current_user.id).first()
@loginRequired if d:
async def getKurs():
"""
Automatische Kurs-Auswahl. \n
Aktives Dualis-Token benötigt.
:return HTML:
"""
dualisUser = Dualis.query.filter_by(uid=currentUser.id).first()
if dualisUser:
cookie = request.cookies.get("cnsc")
timeout = fetchDUALIS.timeOut(dualisUser, cookie, "getKurs")
if timeout:
return timeout
dualisError = False
if not currentUser.kurs:
kurs = await fetchDUALIS.getKurs(dualisUser.token, cookie)
if kurs != 0:
if not fetchRAPLA.getIcal(kurs=kurs):
return render_template('kurs.html', detected=(kurs, dualisError), s="s",
theorie="hidden", praxis="hidden", file=False)
currentUser.kurs = kurs
db.session.commit()
else:
dualisError = True
else:
kurs = currentUser.kurs
currentUser.kurs = kurs
db.session.commit()
else:
dualisError = True
kurs = ""
return render_template('kurs.html', detected=(kurs, dualisError), s="s", theorie="hidden",
praxis="hidden", file=True)
@app.route("/set-up/semester")
@loginRequired
async def getSemester():
"""
Manuelle Semester-Auswahl.
:return HTML:
"""
token = Dualis.query.filter_by(uid=currentUser.id).first().token
cookie = request.cookies.get("cnsc") cookie = request.cookies.get("cnsc")
semesterList = Semesterlist.query.filter_by(uid=currentUser.id).all() timeout = fetchDUALIS.timeOut(d, cookie, "getKurs")
if not semesterList: if timeout:
semester = await semesterDualisToDB([], token, cookie) return timeout
else: e = False
semester = await getSemesterList(currentUser.id, token, cookie) if not current_user.kurs:
return render_template("semester.html", semester=semester, s="s", kurs = await fetchDUALIS.getKurs(d.token, cookie)
theorie="hidden", praxis="hidden") if kurs != 0:
if not fetchRAPLA.getIcal(kurs):
@app.route("/set-up/semester", methods=["POST"]) return render_template('kurs.html', detected=(kurs, e), s="s", theorie="hidden", praxis="hidden",
@loginRequired file=False)
def setSemester(): current_user.kurs = kurs
"""
Speichern der Semester-Auswahl.
:return HTML:
"""
nextArg = request.args.get("next")
if not nextArg:
nextArg = url_for("welcome")
dualisUser = Dualis.query.filter_by(uid=currentUser.id).first()
dualisUser.semester = request.form.get("sem")
db.session.commit()
return redirect(nextArg)
@app.route("/set-up/rapla")
@loginRequired
def chooseRaplas():
"""
Manuelle Rapla-Auswahl.
:return HTML:
"""
raplas = getRaplas()
return render_template("rapla.html", raplas=raplas, s="s", theorie="hidden", praxis="hidden")
@app.route("/set-up/rapla", methods=["POST"])
@loginRequired
async 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":
return redirect(url_for("chooseRaplas"))
if file != "None":
loadUser(currentUser.id).kurs = file[5:-5]
db.session.commit()
elif url != "None":
file = await getNewRapla(url)
if type(file) is not int:
loadUser(currentUser.id).kurs = file[5:-5]
db.session.commit() db.session.commit()
else: else:
return redirect(url_for("error", ecode=900)) e = True
return redirect(url_for("welcome")) else:
kurs = current_user.kurs
@app.route("/log-in") current_user.kurs = kurs
def login():
"""
Login-Maske.
:return HTML:
"""
return render_template("login.html", theorie="hidden", praxis="hidden", s="s")
@app.route("/plan/delete", methods=["POST"])
@loginRequired
async def removeEvent():
day = request.args.get("day")
id = str(request.args.get("id"))
uid = currentUser.id
name = request.args.get("name")
idDB = HiddenVL(uid=uid, eventid=id, id=str(uid) + id, name=name)
db.session.add(idDB)
try:
db.session.commit() db.session.commit()
except IntegrityError: else:
return redirect(url_for("displayRapla", week=day)) e = True
await createCustomCalendar(currentUser.id) kurs = ""
return redirect(url_for("displayRapla", week=day)) return render_template('kurs.html', detected=(kurs, e), s="s", theorie="hidden", praxis="hidden", file=True)
@app.route("/plan/restore/event")
@loginRequired @app.route("/set-up/semester")
async def restoreEvent(): @login_required
id = str(request.args.get("id")) async def getSemester():
uid = currentUser.id """
entry = HiddenVL.query.filter_by(id=str(uid) + id).first() Manuelle Semester-Auswahl.
db.session.delete(entry) :return HTML:
"""
t = Dualis.query.filter_by(uid=current_user.id).first().token
c = request.cookies.get("cnsc")
semesterlist = Semesterlist.query.filter_by(uid=current_user.id).all()
if not semesterlist:
semester = await fetchDUALIS.getSem(t, c)
for i in semester:
semitem = Semesterlist(semestername=i[0], semesterid=i[1], uid=current_user.id, itemid=current_user.id*int(i[1][-7:])//1000000)
db.session.add(semitem)
db.session.commit() db.session.commit()
await createCustomCalendar(currentUser.id) else:
return redirect(url_for("restoreVL")) semester = getSemesterList(current_user.id)
return render_template("semester.html", semester=semester, s="s", theorie="hidden", praxis="hidden")
@app.route("/log-in", methods=["POST"])
async 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")
nextArg = request.args.get("next")
if nextArg:
success = make_response(redirect(nextArg))
else:
success = make_response(redirect(url_for("getKurs")))
user = User.query.filter_by(email=email).first() @app.route("/set-up/semester", methods=["POST"])
tokenAndCookie = await fetchDUALIS.checkUser(email, password) @login_required
if tokenAndCookie[0] == -2: def setSemester():
return redirect(url_for("login", code=-2)) """
if user: Speichern der Semester-Auswahl.
dualisUser = Dualis.query.filter_by(uid=user.id).first() :return HTML:
dualisUser.token = tokenAndCookie[0] """
newCookie = tokenAndCookie[1] n = request.args.get("next")
dualisUser.token_created = time.time() if not n:
db.session.commit() n = url_for("welcome")
loginUser(user) d = Dualis.query.filter_by(uid=current_user.id).first()
if user.kurs: d.semester = request.form.get("sem")
if not dualisUser.semester: db.session.commit()
success = make_response(redirect(url_for("getSemester"))) return redirect(n)
elif not nextArg:
success = make_response(redirect(url_for("welcome")))
success.set_cookie("cnsc", value=newCookie, httponly=True, secure=True)
else:
hashedID = int(hashlib.sha1(email.encode("utf-8")).hexdigest(), 16) % (10 ** 8)
vorname = email.find(".") + 1
nachname = min(email[vorname:].find("."), email[vorname:].find("@"))
name = email[vorname:(vorname + nachname)].capitalize()
new_user = User(email=email, name=name, id=hashedID)
db.session.add(new_user)
db.session.commit()
cookie = tokenAndCookie[1]
newDualis = Dualis(uid=hashedID, token=tokenAndCookie[0], token_created=int(time.time()))
db.session.add(newDualis)
db.session.commit()
loginUser(new_user)
success.set_cookie("cnsc", value=cookie, httponly=True, secure=True)
return success
@app.route("/log-out") @app.route("/set-up/rapla")
@loginRequired @login_required
async def logout(): def chooseRaplas():
""" """
Loggt den User aus. Manuelle Rapla-Auswahl.
:return Empty Token: :return HTML:
""" """
cookie = request.cookies.get("cnsc") r = getRaplas()
dualisUser = Dualis.query.filter_by(uid=currentUser.id).first() return render_template("rapla.html", raplas=r, s="s", theorie="hidden", praxis="hidden")
await fetchDUALIS.logOut(dualisUser.token, cookie)
dualisUser.token = None
@app.route("/set-up/rapla", methods=["POST"])
@login_required
async 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":
return redirect(url_for("chooseRaplas"))
if file != "None":
loadUser(current_user.id).kurs = file[5:-5]
db.session.commit() db.session.commit()
logoutUser() elif url != "None":
redirection = make_response(redirect(url_for("login", code=1, next=url_for("welcome")))) file = await getNewRapla(url)
redirection.set_cookie("cnsc", value="Logged out! Your temporary token " if type(file) is not int:
"on our server and the cookie on your device have been deleted.", User.query.filter_by(id=current_user.id).first().kurs = file[5:-5]
httponly=True, db.session.commit()
secure=True)
return redirection
@app.route("/error")
def error():
"""
Error Page für custom-Errors. \n
TODO: Funktion depreciaten. Ersetzen durch Errors auf den entsprechenden Seiten.
:return HTML:
"""
errorCode = request.args.get("ecode")
if errorCode == "900":
message = "Ungültige RAPLA-URL! Sicher, dass der Link zum DHBW-Rapla führt?"
elif errorCode == "899":
message = "Der Kalender wurde nicht gefunden! Sicher, dass der Link korrekt ist?"
else: else:
message = str(errorCode) return redirect(url_for("error", ecode=900))
return render_template('display-message.html', message=message) return redirect(url_for("welcome"))
@app.route("/error")
@app.errorhandler(HTTPException) @app.route("/log-in")
def handle(e): def login():
"""" """
HTTP-Exception-Handler Login-Maske.
:param e: :return HTML:
:return HTML: """
""" return render_template("login.html", theorie="hidden", praxis="hidden", s="s")
return render_template('display-message.html', message=e)
@app.route("/log-in", methods=["POST"])
async 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")
if n:
success = make_response(redirect(n))
else:
success = make_response(redirect(url_for("getKurs")))
user = User.query.filter_by(email=email).first()
t = await fetchDUALIS.checkUser(email, password)
if t[0] == -2:
return redirect(url_for("login", code=-2))
if user:
dualis = Dualis.query.filter_by(uid=user.id).first()
dualis.token = t[0]
newcookie = t[1]
dualis.token_created = time.time()
db.session.commit()
login_user(user)
if user.kurs:
if not dualis.semester:
success = make_response(redirect(url_for("getSemester")))
elif not n:
success = make_response(redirect(url_for("welcome")))
success.set_cookie("cnsc", value=newcookie, httponly=True, secure=True)
else:
hashid = int(hashlib.sha1(email.encode("utf-8")).hexdigest(), 16) % (10 ** 8)
pname = email.find(".") + 1
ename = min(email[pname:].find("."), email[pname:].find("@"))
name = email[pname:pname + ename].capitalize()
new_user = User(email=email, name=name, id=hashid)
db.session.add(new_user)
cookie = requesthelpers.getCookie(t[1].cookies)
new_dualis = Dualis(uid=hashid, token=t[0], token_created=int(time.time()))
db.session.add(new_dualis)
db.session.commit()
login_user(new_user)
success.set_cookie("cnsc", value=cookie, httponly=True, secure=True)
return success
@app.route("/log-out")
@login_required
async def logout():
"""
Loggt den User aus.
:return Empty Token:
"""
cookie = request.cookies.get("cnsc")
dualis = Dualis.query.filter_by(uid=current_user.id).first()
await fetchDUALIS.logOut(dualis.token, cookie)
dualis.token = None
db.session.commit()
logout_user()
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)
return red
@app.route("/error")
def error():
"""
Error Page für custom-Errors. \n
TODO: Funktion depreciaten. Ersetzen durch Errors auf den entsprechenden Seiten.
:return:
"""
error = request.args.get("ecode")
if error == "900":
msg = "Ungültige RAPLA-URL! Sicher, dass der Link zum DHBW-Rapla führt?"
elif error == "899":
msg = "Der Kalender wurde nicht gefunden! Sicher, dass der Link korrekt ist?"
else:
msg = str(error)
return render_template('display-message.html', message=msg)
@app.route("/error")
@app.errorhandler(HTTPException)
def handle(e):
""""
HTTP-Exception-Handler
"""
return render_template('display-message.html', message=e)
if __name__ == "__main__": if __name__ == "__main__":
initRoutes(flaskApp) app.run(host='0.0.0.0', port=2024, debug=True)
flaskApp.run(host='0.0.0.0', port=2024, debug=True)
else:
initRoutes(flaskApp)

View File

@ -68,11 +68,6 @@ h2 {
justify-content: space-evenly; justify-content: space-evenly;
} }
a {
color: white;
font-size: 100%;
}
button { button {
border-radius: 5px; border-radius: 5px;
border-color: white; border-color: white;
@ -105,8 +100,7 @@ option {
.timeline { .timeline {
display: grid; display: grid;
grid-template-rows: repeat(12, 60px); grid-template-rows: repeat(12, 58px);
padding-top: 50px;
} }
.days { .days {
@ -132,8 +126,7 @@ option {
} }
.room, .room,
.time, .time {
.teacher {
display: block; display: block;
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
@ -201,9 +194,7 @@ nav ul {
left: 0; left: 0;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
font-size: 150%; font-size: 150%;
z-index: 100;
} }
nav li { nav li {

View File

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["cal.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAMF;EACE;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;EAGE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;EAEE;;;AAKF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAKF;EACI;EACA;;;AAIJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAIJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACE;;;AAQF;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAKA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAKA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAKA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA","file":"cal.css"} {"version":3,"sourceRoot":"","sources":["cal.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAMF;EACE;EACA;EACA;EACA;;;AAGF;AAAA;EAEE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;EAEE;;;AAKF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAKF;EACI;EACA;;;AAIJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAIJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACE;;;AAQF;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAKA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAKA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAKA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA;;;AAEA;EACA","file":"cal.css"}

View File

@ -68,11 +68,6 @@ h2 {
justify-content: space-evenly; justify-content: space-evenly;
} }
a {
color: white;
font-size: 100%;
}
button { button {
border-radius: 5px; border-radius: 5px;
border-color: white; border-color: white;
@ -91,7 +86,7 @@ button:hover{
} }
.plusbutton { .plusbutton {
font-size: 1.5em; font-size: 1.5em
} }
form { form {
@ -105,8 +100,7 @@ option {
.timeline { .timeline {
display: grid; display: grid;
grid-template-rows: repeat(12, 60px); grid-template-rows: repeat(12, 58px);
padding-top: 50px;
} }
.days { .days {
@ -134,8 +128,7 @@ option {
} }
.room, .room,
.time, .time {
.teacher {
display: block; display: block;
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
@ -208,9 +201,7 @@ nav ul {
left: 0; left: 0;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
font-size: 150%; font-size: 150%;
z-index: 100;
} }
nav li { nav li {
@ -285,7 +276,7 @@ a.footer:hover {
// Place on Timeline // Place on Timeline
// Generated by genSCSSstarts.py // Generated by genstarts.py
.start-0100 { .start-0100 {
grid-row-start: 1 grid-row-start: 1

View File

@ -1 +0,0 @@
//window.alert("Dualis ist aktuell nicht verfügbar. Dementsprechend sind auch DualHub-Funktionen eingeschränkt.")

View File

@ -22,8 +22,6 @@ nav ul {
left: 0; left: 0;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
z-index: 100;
} }
nav li { nav li {
@ -91,11 +89,6 @@ nav li .bottom {
padding: 20px; padding: 20px;
} }
a {
color: white;
font-size: 110%;
}
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAAA;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAIJ;EACI;EACA;;;AAGJ;AACA;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACH;;;AAGD;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACE;;;AAGF;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA","file":"style.css"} {"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAAA;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAIJ;EACI;EACA;;;AAGJ;AACA;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACH;;;AAGD;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACE;;;AAGF;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA","file":"style.css"}

View File

@ -22,8 +22,6 @@ nav ul {
left: 0; left: 0;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
z-index: 100;
} }
nav li { nav li {
@ -91,11 +89,6 @@ nav li .bottom {
padding: 20px; padding: 20px;
} }
a {
color: white;
font-size: 110%;
}
.container { .container {
display: flex; display: flex;

View File

@ -4,38 +4,39 @@
<title>DualHub</title> <title>DualHub</title>
<link rel="stylesheet" type="text/css" href={{ url_for("static", filename="style.css") }}> <link rel="stylesheet" type="text/css" href={{ url_for("static", filename="style.css") }}>
<meta http-equiv="refresh" content="510"> <meta http-equiv="refresh" content="510">
<script src={{ url_for("static", filename="motd.js") }}></script> <script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script>
{{ turbo() }}
{% block head %} {% block head %}
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
<nav> <nav>
<ul> <ul id = "navbar-items">
{% if theorie=="hidden" and praxis=="hidden" %} {% if theorie=="hidden" and praxis=="hidden" %}
<li class="top"><a class="top" href={{ url_for("welcome", sel="theorie") }}>Theorie</a></li> <li class="top"><a class="top" href={{ url_for("index", sel="theorie") }}>Theorie</a></li>
<li class="top"><a class="top" href={{ url_for("welcome", sel="praxis") }}>Praxis</a></li> <li class="top"><a class="top" href={{ url_for("index", sel="praxis") }}>Praxis</a></li>
{% elif theorie == "hidden" %} {% elif theorie == "hidden" %}
<li class="top"><a class="top" href={{ url_for("welcome", sel="theorie") }}>Theorie</a></li> <li class="top"><a class="top" href={{ url_for("index", sel="theorie") }}>Theorie</a></li>
<li class="top selected"><a class="top selected" href={{ url_for("welcome", sel="praxis") }}>Praxis</a></li> <li class="top selected"><a class="top selected" href={{ url_for("index", sel="praxis") }}>Praxis</a></li>
{% else %} {% else %}
<li class="top selected"><a class="top selected" href={{ url_for("welcome", sel="theorie") }}>Theorie</a></li> <li class="top selected"><a class="top selected" href={{ url_for("index", sel="theorie") }}>Theorie</a></li>
<li class="top"><a class="top" href={{ url_for("welcome", sel="praxis") }}>Praxis</a></li> <li class="top"><a class="top" href={{ url_for("index", sel="praxis") }}>Praxis</a></li>
{% endif %}
<li {{ theorie }}><a {% if s == "n" %} class="selected" {% endif %} href={{ url_for("displayNoten") }}>Noten</a></li>
<li {{ theorie }}><a {% if s == "p" %} class="selected" {% endif %} href={{ url_for("displayRapla") }}>Stundenplan</a></li>
<li {{ praxis }}><a {% if s == "t" %} class="selected" {% endif %} href="">To Dos</a></li>
<li><a {% if s == "s" %} class="selected" {% endif %} href={{ url_for("redKurs") }}>Konfiguration</a></li>
{% if request.endpoint %}
{% if request.endpoint[:7] != "login" %}
<li><a class="bottom" href={{ url_for("logout") }}>Log-Out</a></li>
{% endif %} {% endif %}
{% endif %} <li {{ theorie }}><a {% if s == "n" %} class="selected" {% endif %} href={{ url_for("displayNoten") }}>Noten</a></li>
</ul> <li {{ theorie }}><a {% if s == "p" %} class="selected" {% endif %} href={{ url_for("displayRapla") }}>Stundenplan</a></li>
<li {{ praxis }}><a {% if s == "t" %} class="selected" {% endif %} href="">To Dos</a></li>
<li><a {% if s == "s" %} class="selected" {% endif %} href={{ url_for("redKurs") }}>Konfiguration</a></li>
<li {% block logout %}{% endblock %}><a class="bottom" href={{ url_for("logout") }}>Log-Out</a></li>
</ul>
</nav> </nav>
<div class="cs1"> <turbo-frame>
{% block content %} <div class="cs1" id = 'page'>
{% endblock %} <div id="content">
</div> {% block content %}
{% endblock %}
</div>
</div>
</turbo-frame>
</body> </body>
</html> </html>

View File

@ -4,7 +4,7 @@
{% if file %} {% if file %}
<h1>Wir haben {{ detected[0] }} als deinen Kurs ermittelt. Falls er nicht stimmt, kannst du ihn unten auswählen.</h1> <h1>Wir haben {{ detected[0] }} als deinen Kurs ermittelt. Falls er nicht stimmt, kannst du ihn unten auswählen.</h1>
{% if not request.args.get("next") %} {% if not request.args.get("next") %}
<form id="autoForm" action={{ url_for("getSemester") }}> <form action={{ url_for("getSemester") }}>
{% else %} {% else %}
<form action={{ request.args.get("next") }}> <form action={{ request.args.get("next") }}>
{% endif %} {% endif %}
@ -16,7 +16,7 @@
{% else %} {% else %}
<h1>Dein Kurs konnte leider nicht ermittelt werden. Klicke den Button, um zur Auswahl zu kommen.</h1> <h1>Dein Kurs konnte leider nicht ermittelt werden. Klicke den Button, um zur Auswahl zu kommen.</h1>
{% endif %} {% endif %}
<form id="manualForm" action={{ url_for("chooseRaplas", next=request.args.get("next")) }}> <form action={{ url_for("chooseRaplas", next=request.args.get("next")) }}>
<input type="submit" value="Manuell auswählen!"> <input type="submit" value="Manuell auswählen!">
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,8 @@
{% extends "index.html" %} {% extends "index.html" %}
{% block logout %}
hidden
{% endblock %}
{% block content %} {% block content %}
<div class="cs"> <div class="cs">
<h1>Einloggen</h1> <h1>Einloggen</h1>

21
templates/noten-list.html Normal file
View File

@ -0,0 +1,21 @@
{% for i in range (semester|length) %}
{% if semester[i][1]==sel %}
<h1>Deine Noten im {{ semester[i][0] }}</h1>
{% endif %}
{% endfor %}
<br>
<br>
{% for i in noten %}
{% if i[1]|length == 1 %}
{% if i[1][0][0].isnumeric() or i[1][0][-1].isalpha()%}
<h2>{{ i[0][0] }}: {{ i[1][0] }} (Credits: {{ i[2][0] }})</h2>
{% else %}
<h2>{{ i[0][0] }}: {{ i[1][0][-3:] }} (Credits: {{ i[2][0] }})</h2>
{% endif %}
{% else %}
<h2>{{ i[0][0] }} (Credits: {{ i[2][0] }}):</h2>
{% for e in i[1] %}
<h2>&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;{{ e }}</h2>
{% endfor %}
{% endif %}
{% endfor %}

View File

@ -4,27 +4,11 @@
<meta http-equiv="Refresh" content="600"> <meta http-equiv="Refresh" content="600">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% for i in range (semester|length) %} <turbo-frame>
{% if semester[i][1]==sel %} <div id="noten">
<h1>Deine Noten im {{ semester[i][0] }}</h1> {% include "noten-list.html" %}
{% endif %} </div>
{% endfor %} </turbo-frame>
<br>
<br>
{% for i in noten %}
{% if i[1]|length == 1 %}
{% if i[1][0][0].isnumeric() or i[1][0][-1].isalpha()%}
<h2>{{ i[0][0] }}: {{ i[1][0] }} (Credits: {{ i[2][0] }})</h2>
{% else %}
<h2>{{ i[0][0] }}: {{ i[1][0][-3:] }} (Credits: {{ i[2][0] }})</h2>
{% endif %}
{% else %}
<h2>{{ i[0][0] }} (Credits: {{ i[2][0] }}):</h2>
{% for e in i[1] %}
<h2>&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;{{ e }}</h2>
{% endfor %}
{% endif %}
{% endfor %}
<br> <br>
<br> <br>
<form id= "dropdown" method="post" action={{ url_for ("displayNoten") }}> <form id= "dropdown" method="post" action={{ url_for ("displayNoten") }}>

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Vorlesungsplan</title> <title>Vorlesungsplan</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='cal.css') }}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='cal.css') }}">
<script src={{ url_for("static", filename="motd.js") }}></script> <script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script>
</head> </head>
<body> <body>
<nav> <nav>
@ -63,26 +63,12 @@
<div class="events"> <div class="events">
{% for i in events %} {% for i in events %}
{% if i["weekday"] == e %} {% if i["weekday"] == e %}
{% with event=i %} <div class="event start-{{ i["start"][:2]+i["start"][3:] }} end-{{ i["end"][:2]+i["end"][3:]}}">
<div class="event start-{{ event["start"][:2]+event["start"][3:] }} end-{{ event["end"][:2]+event["end"][3:]}}"> {% block event %} {% endblock %}
{% if login %} <p class="title">{{ i["name"] }}</p>
<div class="userbuttons"> <p class="room">{{ i["room"] }}</p>
<form method="post" action={{ url_for ("removeEvent", id=event["id"], day=date, name=event["name"]) }}> <p class="time">{{ i["start"] }} - {{ i["end"] }} ({{ i["dur"] }}h)</p>
<button>👁</button> </div>
</form>
<form>
<button class="plusbutton">+</button>
</form>
</div>
{% endif %}
<p class="title">{{ event["name"] }}</p>
<p class="room">{{ event["room"] }}</p>
{% for teacher in event["teacher"] %}
<p class="teacher">{{ teacher }}</p>
{% endfor %}
<p class="time">{{ event["start"] }} - {{ event["end"] }} ({{ event["dur"] }}h)</p>
</div>
{% endwith %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>

View File

@ -5,8 +5,11 @@
{% block startcontent %} {% block startcontent %}
<h2>{{ name }}s Vorlesungsplan</h2> <h2>{{ name }}s Vorlesungsplan</h2>
{% endblock %} {% endblock %}
{% block endcontent %} {% block event %}
<a href={{ url_for("deliverCalendar", uid=uid) }}>Kalender-Abo</a> <div class="userbuttons">
<br> <button>👁</button>
<a href={{ url_for("restoreVL") }}>Ausgeblendete Vorlesungen wieder einblenden</a> <button class="plusbutton">+</button>
</div>
{% endblock %}
{% block endcontent %}
{% endblock %} {% endblock %}

View File

@ -1,7 +1,7 @@
{% extends "index.html" %} {% extends "index.html" %}
{% block content %} {% block content %}
<h1>Verfügbare Raplas </h1> <h1>Verfügbare Raplas </h1>
<form id="dbForm" method="post" action={{ url_for ("getRapla") }}> <form method="post" action={{ url_for ("getRapla") }}>
<label for="file">Vefügbaren RAPLA wählen! </label> <label for="file">Vefügbaren RAPLA wählen! </label>
<select name="file" id="file"> <select name="file" id="file">
@ -13,7 +13,7 @@
<input type="submit" value="Importieren!"> <input type="submit" value="Importieren!">
</form> </form>
<h1>Eigenen Rapla hinzufügen</h1> <h1>Eigenen Rapla hinzufügen</h1>
<form id="manualForm" method="post" action={{ url_for ("getRapla") }}> <form method="post" action={{ url_for ("getRapla") }}>
<label for="url">Rapla-URL eingeben, falls Du Deinen Kurs nicht siehst:</label> <label for="url">Rapla-URL eingeben, falls Du Deinen Kurs nicht siehst:</label>
<input type="url" name="url" id="url"> <input type="url" name="url" id="url">
<input type="submit" value="Importieren!"> <input type="submit" value="Importieren!">

View File

@ -1,12 +0,0 @@
{% extends "index.html" %}
{% block content %}
<h2>Ausgeblendete Vorlesungen</h2>
{% for event in events %}
<li>
<a href={{ url_for("restoreEvent", id=event.eventid) }}>{{ event.name }}</a>
</li>
{% endfor %}
{% if events | length == 0 %}
<a href = {{ url_for("displayRapla") }}>Zurück zum Stundenplan (keine ausgeblendeten Vorlesungen) </a>
{% endif %}
{% endblock %}

16
templates/testpages.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test Pages</title>
</head>
<body>
<a href={{ url_for("kurs") }} > kurs </a> <br>
<a href={{ url_for("login") }} > login </a><br>
<a href={{ url_for("displayNoten") }} > noten </a><br>
<a href={{ url_for("plananon", kurs="tinf22b3") }} > plan-anon </a><br>
<a href={{ url_for("planuser") }} > plan-user </a><br>
<a href={{ url_for("chooseRaplas") }} > rapla </a><br>
<a href={{ url_for("getSemester") }} > semester </a><br>
</body>
</html>

View File

@ -1,3 +0,0 @@
email = "EMAIL-GOES-HERE"
password = "PASSWORD-GOES-HERE"
kurs_url = "RAPLA-URL-GOES-HERE"

View File

@ -1,182 +0,0 @@
import pytest
from bs4 import BeautifulSoup
import fetchRAPLA
import routing
import init
from tests_examples import login_data
@pytest.fixture()
def app():
"""
Erstellt die App und konfiguriert sie zum Test-Modus
:yield app:
"""
app = init.create(testing=True)
app.config.update({
"TESTING": True,
})
routing.initRoutes(app)
yield app
@pytest.fixture()
def client(app):
"""
Liefert einen Test-Client
:param app:
:return client:
"""
return app.test_client(use_cookies=True)
def login(client):
"""
Hilfsfunktion, die den Client einloggt
:param client:
:return Bool (true if successful, false otherwise):
"""
client.post('/log-in', data=dict(email=login_data.email, password=login_data.password),
follow_redirects=True)
cookie = client.get_cookie("cnsc")
try:
return len(cookie.value) == 32 # CNSC-Länge: 32 → Wenn der Cookie so lang ist, ist man erfolgreich eingeloggt.
except AttributeError:
return False
def test_login_blackbox(client):
"""
Testet die Login-Funktion
:param client:
"""
loginpage = client.get("/log-in", follow_redirects=True)
assert b"Einloggen" in loginpage.data
assert loginpage.status_code == 200
loginpage_html = BeautifulSoup(loginpage.text, "lxml")
login_form = loginpage_html.find("form")
login_action = login_form.get("action")
login_request = client.post(login_action, data=dict(email=login_data.email, password=login_data.password),
follow_redirects=True)
assert login_request.status_code == 200
cookie = client.get_cookie("cnsc")
assert len(cookie.value) == 32 # CNSC-Länge: 32 → Wenn der Cookie so lang ist, ist man erfolgreich eingeloggt.
def test_kurssetup_blackbox(client):
"""
Testet die Konfiguration eines Kurses
:param client:
"""
if login(client):
kurspage = client.get("/set-up", follow_redirects=True)
assert kurspage.status_code == 200
kurspage_html = BeautifulSoup(kurspage.text, "lxml")
kursbutton = kurspage_html.find("form", {"id": "manualForm"})
kursbutton_action = kursbutton.get("action")
planpage = client.get(kursbutton_action, follow_redirects=True)
assert planpage.status_code == 200
planpage_html = BeautifulSoup(planpage.text, "lxml")
planpage_form = planpage_html.find("form", {"id": "manualForm"})
planpage_action = planpage_form.get("action")
set_request = client.post(planpage_action, data=dict(url=login_data.kurs_url), follow_redirects=True)
assert set_request.status_code == 200
assert b"Willkommen, " in set_request.data
else:
assert False
def test_semestersetup_blackbox(client):
"""
Testet die Konfiguration eines Semesters
:param client:
"""
if login(client):
semesterpage = client.get("/set-up/semester", follow_redirects=True)
assert semesterpage.status_code == 200
semesterpageHTML = BeautifulSoup(semesterpage.text, "lxml")
semesterform = semesterpageHTML.find("form")
semesterformAction = semesterform.get("action")
semesterformOptions = semesterform.find_all("option")
nextpage = client.post(semesterformAction, data=dict(sem=semesterformOptions[-1].get("value")),
follow_redirects=True)
assert nextpage.status_code == 200
assert b"Willkommen, " in nextpage.data
else:
assert False
def test_noten_blackbox(client):
"""
Testet das Abrufen der Noten aus zwei verschiedenen Semestern
:param client:
"""
if login(client):
notenpage = client.get("/theorie/noten", follow_redirects=True)
assert notenpage.status_code == 200
notenpageHTML = BeautifulSoup(notenpage.text, "lxml")
notenpageHeading = notenpageHTML.find("h1")
notenpageForm = notenpageHTML.find("form")
notenpageAction = notenpageForm.get("action")
notenpageSelection = notenpageForm.find("select")
notenpageOptions = notenpageSelection.find_all("option")
notenpageSemester = "Not found!"
nextpage = "Not found!"
for i in notenpageOptions:
if i.get("selected") == "":
notenpageSemester = i.text[:-1]
else:
nextpage = i.get("value")
assert notenpageSemester.encode("utf-8") in notenpageHeading.encode("utf-8")
nextpage = client.post(notenpageAction, data=dict(sem=nextpage), follow_redirects=True)
assert nextpage.status_code == 200
else:
assert False
def test_logout_blackbox(client):
"""
Testet die Logout-Funktion
:param client:
"""
if login(client):
loginpage = client.get("/log-out", follow_redirects=True)
assert loginpage.status_code == 200
assert b"Einloggen" in loginpage.data
cookie = client.get_cookie("cnsc")
assert len(cookie.value) != 32 # CNSC-Länge: 32 → CNSC darf ausgeloggt nicht gesetzt sein
else:
assert False
@pytest.mark.asyncio()
async def test_url_anweisung_whitebox(app):
"""
Testet einen Pfad des URL-Imports
:param app:
"""
with app.app_context():
raplaPage = await fetchRAPLA.getNewRapla("http://www.rapla.dhbw-karlsruhe.de/rapla?page=calendar&user="
"vollmer&file=tinf22b3", True) #HTML
assert "TINF22B3" in raplaPage
raplaFile = await fetchRAPLA.getNewRapla("http://rapla.dhbw-karlsruhe.de/rapla?page=ical&user=vollmer"
"&file=tinf22b3", True) #ICAL
assert "TINF22B3" in raplaFile
@pytest.mark.asyncio()
async def test_url_entscheidung_whitebox(app):
"""
Testet alle Pfade des URL-Imports, die mit einer fehlerfreien Datei enden
:param app:
"""
with app.app_context():
await test_url_anweisung_whitebox(app)
raplaFile = await fetchRAPLA.getNewRapla("https://rapla.dhbw-karlsruhe.de/rapla?key=5h7oySnUbC4A59dSScuZ"
"lPHhaNFS3OaaP-0UTlOEPu-NcWfZ-gMhnSpHZmYCPcIe", True) #ICAL
assert "TMB22" in raplaFile
raplaPage = await fetchRAPLA.getNewRapla("http://www.rapla.dhbw-karlsruhe.de/rapla?key=ah9tAVphi"
"caj4FqCtMVJchAs9fh0Dt89jA8Td4kEi21V0i2mlUEpycpIVw5jSY5T",
True) #HTML
assert "TMT22B1" in raplaPage

View File

@ -0,0 +1,97 @@
import datetime
import flask
from flask import render_template, Flask, url_for, redirect
app = Flask(__name__)
sampleweek = ([{'start': '08:30', 'end': '11:00', 'dur': '2:30', 'name': "Rechnerarchitekturen",
'room': "A266 Hörsaal", 'weekday': 0, 'day': 11},
{'start': '09:30', 'end': '12:00', 'dur': '2:30', 'name': "Netztechnik 1", 'room': "", 'weekday': 1,
'day': 12},
{'start': '08:30', 'end': '12:00', 'dur': '3:30', 'name': "Info3", 'room': "", 'weekday': 2,
'day': 13},
{'start': '08:30', 'end': '12:00', 'dur': '3:30', 'name': "Info3", 'room': "", 'weekday': 3,
'day': 14}, {'start': '11:00', 'end': '12:30', 'dur': '1:30', 'name': "Systemnahes Programmieren",
'room': "A266 Hörsaal", 'weekday': 0, 'day': 11},
{'start': '13:00', 'end': '16:15', 'dur': '3:15', 'name': "Java", 'room': "A266 Hörsaal",
'weekday': 3, 'day': 14}], [{'day': 11, 'short': 'mon', 'long': 'Montag', 'mensa': [
'Frikadelle Hausfrauen Art mit Kräutersoße und Risoleekartoffeln Kräutersoße',
'Veganes Gemüseschnitzel Risoleekartoffeln Kräutersoße', 'Kaiserschmarrn mit Rosinen und Apfelmus Vanillesoße']},
{'day': 12, 'short': 'tue', 'long': 'Dienstag', 'mensa': [
'Alaska Seelachsfilet in Backteig hausgemachter Kartoffelsalat Dip',
'Gemüsefrikadellen hausgemachter Kartoffelsalat Dip']},
{'day': 13, 'short': 'wed', 'long': 'Mittwoch', 'mensa': [
'Roter Curry - Gemüseeintopf mit Hähnchenstreifen und Baguettebrötchen',
'Roter Curry - Gemüseeintopf mit Sojastreifen und Baguettebrötchen',
'Gebratene Gnocchis mit Karotten und Schnittlauchsoße']},
{'day': 14, 'short': 'thu', 'long': 'Donnerstag', 'mensa': [
'Pasta mit Paprika, getrockneten Tomaten, Pinienkernen, Basilikum und Reibekäse',
'2010: Königsberger Klopse in Kapernsoße und Salzkartoffeln',
'Pasta mit Hackfleisch - Champignon - Soße, Reibekäse']},
{'day': 15, 'short': 'fri', 'long': 'Freitag', 'mensa': [
'Griechische Nudelpfanne mit Sojastreifen, Gemüse, Pinienkerne und Tomatensoße',
'Griechische Nudelpfanne mit Geflügel, Gemüse, Pinienkerne und Tomatensoße']}],
datetime.date(2023, 12, 6), datetime.date(2023, 12, 20), 'Dezember 2023')
@app.route("/")
def index():
return render_template('testpages.html')
@app.route("/kurs")
def kurs():
return render_template("kurs.html", detected=("TINF22B3", False))
@app.route("/login")
def login():
return render_template("login.html")
@app.route("/noten")
def displayNoten():
return render_template("noten.html", noten=[["Info", "nicht bestanden", 2000]],
semester=[["Sommersemester", "SoSe"]], sel="SoSe")
@app.route("/plan/<string:kurs>")
def plananon(kurs):
return render_template("plan-anon.html", events=sampleweek[0], eventdays=sampleweek[1], kurs=kurs,
prev=str(sampleweek[2])[:10], next=str(sampleweek[3])[:10], mon=sampleweek[4])
@app.route("/plan")
def planuser():
return render_template("plan-user.html", events=sampleweek[0], eventdays=sampleweek[1], kurs=kurs,
prev=str(sampleweek[2])[:10], next=str(sampleweek[3])[:10], mon=sampleweek[4], name="Studi")
@app.route("/rapla")
def chooseRaplas():
return render_template("rapla.html", raplas=[["TINF22B3"], ["TINF22B3"], ["TINF22B3"]])
@app.route("/semester")
def getSemester():
return render_template("semester.html", semester=[["Sommersemester 2023", "SoSeID"]])
@app.route("/setsemester", methods=["POST"])
def setSemester():
return redirect(url_for("index"))
@app.route("/getrapla", methods=["POST"])
def getRapla():
return redirect(url_for("index"))
@app.route("/login", methods=["POST"])
def login_post():
return redirect(url_for("index"))
if __name__ == "__main__":
app.run(host='0.0.0.0', port=2024, debug=True)

View File

@ -8,7 +8,7 @@ from bs4 import BeautifulSoup
from celery import Celery from celery import Celery
import fetchDUALIS import fetchDUALIS
from login_data import password, email #CREATE LOCAL login_data.py!!! from login_data import passwort, email #CREATE LOCAL login_data.py!!!
app = Flask(__name__) app = Flask(__name__)
app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0' app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0'
@ -26,7 +26,7 @@ headers = {
} }
url = "https://dualis.dhbw.de/scripts/mgrqispi.dll" url = "https://dualis.dhbw.de/scripts/mgrqispi.dll"
fpw = urllib.parse.quote(password, safe='', encoding=None, errors=None) fpw = urllib.parse.quote(passwort, safe='', encoding=None, errors=None)
fmail = urllib.parse.quote(email, safe='', encoding=None, errors=None) fmail = urllib.parse.quote(email, safe='', encoding=None, errors=None)
@ -93,7 +93,6 @@ async def checkUser_async():
""" """
# noinspection DuplicatedCode # noinspection DuplicatedCode
async with httpx.AsyncClient() as s: async with httpx.AsyncClient() as s:
# noinspection DuplicatedCode
content = (f'usrname={fmail}&pass={fpw}&ARGUMENTS=clino%2Cusrname%2Cpass%2Cmenuno%2Cmenu_type%2Cbrowser' content = (f'usrname={fmail}&pass={fpw}&ARGUMENTS=clino%2Cusrname%2Cpass%2Cmenuno%2Cmenu_type%2Cbrowser'
f'%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK') f'%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK')
response = await s.post(url=url, headers=headers, content=content) response = await s.post(url=url, headers=headers, content=content)
@ -168,7 +167,6 @@ async def getResults_async(token, cookie, resl):
async with httpx.AsyncClient() as s: async with httpx.AsyncClient() as s:
response = await s.get(url=url + "?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N" + token + response = await s.get(url=url + "?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N" + token +
",-N000307," + ",-N" + resl, headers=headers) ",-N000307," + ",-N" + resl, headers=headers)
# noinspection DuplicatedCode
html = BeautifulSoup(response.content.decode("utf-8"), 'lxml') html = BeautifulSoup(response.content.decode("utf-8"), 'lxml')
table = html.find('table', attrs={"class": "nb list"}) table = html.find('table', attrs={"class": "nb list"})
body = table.find("tbody") body = table.find("tbody")
@ -185,7 +183,7 @@ async def getResults_async(token, cookie, resl):
col[2] = i col[2] = i
i += 1 i += 1
vorlist += [col[1:4]] vorlist += [col[1:4]]
# noinspection DuplicatedCode
extrakurse = await asyncio.gather(*tasks, return_exceptions=True) extrakurse = await asyncio.gather(*tasks, return_exceptions=True)
for i in vorlist: for i in vorlist:
@ -194,7 +192,6 @@ async def getResults_async(token, cookie, resl):
i[e] = extrakurse[i[e]] i[e] = extrakurse[i[e]]
return vorlist[:-1] return vorlist[:-1]
# noinspection DuplicatedCode # noinspection DuplicatedCode
async def getPruefung_async(s, url): async def getPruefung_async(s, url):
response = await s.get("https://dualis.dhbw.de" + url, headers=headers) response = await s.get("https://dualis.dhbw.de" + url, headers=headers)

View File

@ -1,5 +1,5 @@
[uwsgi] [uwsgi]
mount = /dualhub=routing:flaskApp mount = /dualhub=routing:app
manage-script-name = true manage-script-name = true
pidfile = dualhub_flask.pid pidfile = dualhub_flask.pid
master = true master = true