22 Commits

Author SHA1 Message Date
8021fdfaf8 Merge branch 'refs/heads/main' into turbo
# Conflicts:
#	init.py
2024-04-10 13:53:51 +02:00
447800ad73 complete rapla async 2024-04-10 13:10:56 +02:00
ccb088e36d optimize mensa async 2024-04-09 11:04:18 +02:00
0d31d84d48 complete mensa async 2024-04-09 08:50:05 +02:00
490ee7f02f complete dualis async 2024-04-09 00:16:59 +02:00
211ec18887 async test 2024-04-08 20:48:34 +02:00
8be441bab9 init turbo 2024-04-05 11:22:14 +02:00
56ec2cfb15 Refreshs 2024-04-02 17:58:30 +02:00
4260ca0e92 Mensa-Fix 2024-01-22 18:10:41 +01:00
3b8cf7e53d Nested Noten 2024-01-11 14:50:28 +01:00
b8c2bea4ae Content Security Policy 2023-12-15 15:19:26 +01:00
937bf9787c Analytics-Link 2023-12-15 10:18:37 +01:00
c77f6017d4 Quickfix Rapla-Pipeline 2023-12-15 09:55:40 +01:00
8ddd7a972c Navbar-Hover + Analytics 2023-12-15 09:29:59 +01:00
1c971975dd Plan-Buttons 2023-12-14 22:29:39 +01:00
2d964e0a29 Plan-Animation 2023-12-14 22:25:19 +01:00
4f9fb5ac4d Quickfix Plan-Anon 2023-12-14 22:11:51 +01:00
fc97411177 Errorpage + RAPLA-Styling 2023-12-14 21:43:13 +01:00
a6372086db Launch prep 2023-12-14 18:49:45 +01:00
64115fe2d0 Merge remote-tracking branch 'origin/Frontend' into Backend
# Conflicts:
#	static/style.css
#	templates/index.html
2023-12-14 13:13:12 +01:00
07d4dd098f Dasboard 2023-12-14 13:10:22 +01:00
65d14f51cd Dashboard 2023-12-14 13:09:36 +01:00
31 changed files with 1429 additions and 486 deletions

View File

@ -1,3 +1,6 @@
import time
import asyncio
import icalendar import icalendar
import datetime import datetime
import recurring_ical_events import recurring_ical_events
@ -10,7 +13,7 @@ months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August"
"November", "Dezember"] "November", "Dezember"]
def getWeek(weekstart: datetime, file: str, showsat: bool): async def getWeek(weekstart: datetime, file: str, showsat: bool):
""" """
Liefert alle Events einer Woche zurück. \n Liefert alle Events einer Woche zurück. \n
Wochenstart wird automatisch auf den Montag der Woche gelegt. \n Wochenstart wird automatisch auf den Montag der Woche gelegt. \n
@ -67,10 +70,10 @@ def getWeek(weekstart: datetime, file: str, showsat: bool):
"day": estart.day "day": estart.day
} }
eventl += [eventdict] eventl += [eventdict]
return eventl, daylist(start_date, showsat), prevw, nextw, mon return eventl, await daylist(start_date, showsat), prevw, nextw, mon
def daylist(weekstart: datetime, showsat: bool): async def daylist(weekstart: datetime, showsat: bool):
""" """
Gibt die Essen einer Woche zurück. Gibt die Essen einer Woche zurück.
:param weekstart: :param weekstart:
@ -83,15 +86,19 @@ def daylist(weekstart: datetime, showsat: bool):
r = 6 r = 6
else: else:
r = 5 r = 5
essen = []
for i in range(r): for i in range(r):
essen = getMeals(weekday) essen += [getMeals(weekday)]
dayl += [{ dayl += [{
"day": weekday.day, "day": weekday.day,
"short": shortnames[i], "short": shortnames[i],
"long": longnames[i], "long": longnames[i],
"mensa": essen "mensa": i
}] }]
weekday += datetime.timedelta(days=1) weekday += datetime.timedelta(days=1)
essenl = await asyncio.gather(*essen, return_exceptions=True)
for day in range(r):
dayl[day]["mensa"] = essenl[day]
return dayl return dayl

View File

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

View File

@ -1,9 +1,10 @@
import requests
import urllib.parse import urllib.parse
import time import time
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from flask import redirect, url_for from flask import redirect, url_for
from init import Dualis from init import Dualis
import asyncio
import httpx
headers = { headers = {
'Cookie': 'cnsc=0', 'Cookie': 'cnsc=0',
@ -16,31 +17,32 @@ headers = {
url = "https://dualis.dhbw.de/scripts/mgrqispi.dll" url = "https://dualis.dhbw.de/scripts/mgrqispi.dll"
def checkUser(email: str, password: str): 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, Session): :return (Token, Session):
""" """
s = requests.Session() async with httpx.AsyncClient() as s:
fpw = urllib.parse.quote(password, safe='', encoding=None, errors=None) fpw = urllib.parse.quote(password, 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)
payload = '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'
'%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK') f'%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK')
response = s.post(url, headers=headers, data=payload) 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
komma = refresh[arg:].find(",") komma = refresh[arg:].find(",")
except KeyError: cookie = s.cookies.get("cnsc")
return -2, s except KeyError:
token = refresh[arg:komma + arg] return -2, s
return token, s token = refresh[arg:komma + arg]
return token, cookie
def getKurs(token: int, cookie: str): async def getKurs(token: int, cookie: str):
""" """
Bestimmt aus der ersten Prüfung den Kursbezeichner des Users. Bestimmt aus der ersten Prüfung den Kursbezeichner des Users.
TODO: Umstellen auf Bezeichner INKL. Standort TODO: Umstellen auf Bezeichner INKL. Standort
@ -51,34 +53,34 @@ def getKurs(token: int, cookie: str):
try: try:
headers["Cookie"] = "cnsc=" + cookie headers["Cookie"] = "cnsc=" + cookie
token = str(token) token = str(token)
response = requests.request("GET", url + async with httpx.AsyncClient as s:
"?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, data={}) headers=headers)
html = BeautifulSoup(response.text, 'lxml') html = BeautifulSoup(response.text, 'lxml')
link = html.body.find('a', attrs={'id': "Popup_details0001"})['href'] link = html.body.find('a', attrs={'id': "Popup_details0001"})['href']
response = requests.request("GET", url + link[21:], headers=headers, data={}) 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
start = content.find(" ") + 4 start = content.find(" ") + 4
end = start + (content[start:].find(" ")) end = start + (content[start:].find(" "))
kurs = content[start:end] kurs = content[start:end]
except AttributeError: except AttributeError:
kurs = 0 kurs = 0
return kurs return kurs
def logOut(token: int, cookie: str): async def logOut(token: int, cookie: str):
""" """
Invalidiert Token und Cookie bei Dualis. Invalidiert Token und Cookie bei Dualis.
:param token: :param token:
:param cookie: :param cookie:
""" """
headers["Cookie"] = "cnsc=" + cookie headers["Cookie"] = "cnsc=" + cookie
requests.request("GET", url + "?APPNAME=CampusNet&PRGNAME=LOGOUT&ARGUMENTS=-N" + str(token) async with httpx.AsyncClient() as s:
+ ", -N001", headers=headers, data={}) await s.get(url=f"{url}?APPNAME=CampusNet&PRGNAME=LOGOUT&ARGUMENTS=-N{token}, -N001", headers=headers)
def getSem(token: int, cookie: str): async def getSem(token: int, cookie: str):
""" """
Liefert die Liste aller auf Dualis verfügbaren Semester. Liefert die Liste aller auf Dualis verfügbaren Semester.
:param token: :param token:
@ -87,21 +89,21 @@ def getSem(token: int, cookie: str):
""" """
headers["Cookie"] = "cnsc=" + cookie headers["Cookie"] = "cnsc=" + cookie
token = str(token) token = str(token)
response = requests.request("GET", url + async with httpx.AsyncClient() as s:
"?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, data={}) headers=headers)
html = BeautifulSoup(response.text, 'lxml') html = BeautifulSoup(response.text, 'lxml')
select = html.find('select') select = html.find('select')
select = select.find_all(value=True) select = select.find_all(value=True)
optlist = [] optlist = []
for i in select: for i in select:
t = i.text.replace("Wi", "Winter").replace("So", "Sommer") t = i.text.replace("Wi", "Winter").replace("So", "Sommer")
t = t.replace("Se", "semester") t = t.replace("Se", "semester")
optlist += [[t, i['value']]] optlist += [[t, i['value']]]
return optlist return optlist
def getResults(token, cookie: str, resl: str): async def getResults(token, cookie: str, resl: str):
""" """
Liefert die Liste aller Prüfungsergebnisse eines Semesters. Liefert die Liste aller Prüfungsergebnisse eines Semesters.
:param token: :param token:
@ -110,42 +112,57 @@ def getResults(token, cookie: str, resl: str):
:return [[Name, Note, Credits], ...]: :return [[Name, Note, Credits], ...]:
""" """
headers["Cookie"] = "cnsc=" + cookie headers["Cookie"] = "cnsc=" + cookie
response = requests.request("GET", url + "?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N" + token + async with httpx.AsyncClient() as s:
",-N000307," + ",-N" + resl, headers=headers, data={}) response = await s.get(
html = BeautifulSoup(response.content.decode("utf-8"), 'lxml') url=f"{url}?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N{token},-N000307,,-N{resl}",
table = html.find('table', attrs={"class": "nb list"}) headers=headers)
body = table.find("tbody") html = BeautifulSoup(response.content.decode("utf-8"), 'lxml')
vorl = body.find_all("tr") table = html.find('table', attrs={"class": "nb list"})
vorlist = [] body = table.find("tbody")
for row in vorl: vorl = body.find_all("tr")
cols = row.find_all("td") vorlist = []
col = [e.text.strip() for e in cols] tasks = []
if len(col) != 0: i = 0
if len(col[4]) == 0: for row in vorl:
col[2] = getPruefung(row.find("a")["href"]) cols = row.find_all("td")
col = [[e.text.strip()] for e in cols]
if len(col) != 0:
if len(col[4][0]) == 0:
tasks += [getPruefung(s, row.find("a")["href"])]
col[2] = i
i += 1
vorlist += [col[1:4]] vorlist += [col[1:4]]
return vorlist notlisted = await asyncio.gather(*tasks, return_exceptions=True)
for i in vorlist:
for e in range(0, len(i)):
if isinstance(i[e], int):
i[e] = notlisted[i[e]]
return vorlist[:-1]
def getPruefung(url): async def getPruefung(s, url):
""" """
Ermittelt Noten "geschachtelter" Prüfungen, die nicht auf der Hauptseite angezeigt werden. 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. TODO: Namen der spezifischen Prüfungen auch zurückgeben, um Zusammensetzung zu spezifizieren.
:param s:
:param url: :param url:
:return: :return list:
""" """
response = requests.request("GET", "https://dualis.dhbw.de" + url, headers=headers, data={}) 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")
ret = " " ret = []
for row in pruefung: for row in pruefung:
cols = row.find_all("td") cols = row.find_all("td")
col = [e.text.strip() for e in cols] col = [e.text.strip() for e in cols]
if len(col) == 6: if len(col) == 6 and len(col[3]) <= 13:
ret = col[3][:3] if len(col[3]) != 0:
if ret[1] != ",": ret += [col[0] + ": " + col[3][:3]]
ret = "noch nicht gesetzt" if ret[-1][0] == ':':
ret[-1] = "Gesamt" + ret[-1]
if len(ret) == 0:
ret = ["Noch nicht gesetzt"]
return ret return ret

View File

@ -1,13 +1,16 @@
import json import json
from init import db, Meals, scheduler
import asyncio
from init import db, Meals, scheduler, app
import datetime import datetime
import requests
import time import time
import httpx
nomeal = 'Essen nicht (mehr) verfügbar' nomeal = 'Essen nicht (mehr) verfügbar'
def getMeals(day: datetime): async def getMeals(day: datetime):
""" """
Liefert alle Mahlzeiten eines Tages. \n Liefert alle Mahlzeiten eines Tages. \n
Befinden sie sich schon in der Datenbank, werden diese zurückgegeben. \n Befinden sie sich schon in der Datenbank, werden diese zurückgegeben. \n
@ -23,10 +26,10 @@ def getMeals(day: datetime):
essen += [i.name] essen += [i.name]
essen.sort(key=len, reverse=True) essen.sort(key=len, reverse=True)
return essen return essen
return getMealsFromAPI(day, dbentry=True) return await getMealsFromAPI(day, dbentry=True)
def getMealsFromAPI(day: str, dbentry: bool = False): async def getMealsFromAPI(day: str, dbentry: bool = False):
""" """
Fragt die Mensa-API nach den Mahlzeiten eines Tages ab. \n Fragt die Mensa-API nach den Mahlzeiten eines Tages ab. \n
Wenn dbentry: Schreibt die Ergebnisse in die Datenbank. \n Wenn dbentry: Schreibt die Ergebnisse in die Datenbank. \n
@ -35,47 +38,46 @@ def getMealsFromAPI(day: str, dbentry: bool = False):
:param dbentry: :param dbentry:
:return [Name1, Name2, ...]: :return [Name1, Name2, ...]:
""" """
url = "https://dh-api.paulmartin.cloud/plans/" + day + "?canteens=erzberger" async with httpx.AsyncClient() as s:
response = requests.request("GET", url) response = await s.get(url=f"https://dh-api.paulmartin.cloud/plans/{day}?canteens=erzberger")
response = response.content response = response.content
jres = json.loads(response.decode("utf-8")) jres = json.loads(response.decode("utf-8"))
essen = [] essen = []
try:
num = len(jres["data"][0]["lines"])
for i in range(num):
try:
jsmeal = jres["data"][0]["lines"][i]["meals"]
cont = True
except IndexError:
essen = []
cont = False
if cont:
for e in range(len(jsmeal)):
ji = jsmeal[e]
name = ji["name"]
if pricetofloat(ji["price"]) >= 1.1:
vegan = ji["classifiers"].count("VG") == 1
schwein = ji["classifiers"].count("S") == 1
if vegan:
veget = True
else:
veget = ji["classifiers"].count("VEG") == 1
if veget:
if name.count("Reibekäse") > 0:
vegan = True
try: essen += [name]
num = len(jres["data"][0]["lines"]) if dbentry:
for i in range(num): mid = int(time.time() * 1000) % 100000
try: neu = Meals(date=day, name=name, id=mid, vegan=vegan, vegetarian=veget, schwein=schwein)
jsmeal = jres["data"][0]["lines"][i]["meals"] db.session.add(neu)
cont = True db.session.commit()
except IndexError: if not essen:
essen = [] essen = [nomeal]
cont = False except KeyError:
if cont:
for e in range(len(jsmeal)):
ji = jsmeal[e]
name = ji["name"]
if pricetofloat(ji["price"]) >= 1.1:
vegan = ji["classifiers"].count("VG") == 1
schwein = ji["classifiers"].count("S") == 1
if vegan:
veget = True
else:
veget = ji["classifiers"].count("VEG") == 1
if veget:
if name.count("Reibekäse") > 0:
vegan = True
essen += [name]
if dbentry:
mid = int(time.time()*1000) % 100000
neu = Meals(date=day, name=name, id=mid, vegan=vegan, vegetarian=veget, schwein=schwein)
db.session.add(neu)
db.session.commit()
if not essen:
essen = [nomeal] essen = [nomeal]
except KeyError: return essen
essen = [nomeal]
return essen
def pricetofloat(price: str): def pricetofloat(price: str):
@ -99,37 +101,47 @@ def formatDay(day: datetime):
:param day: :param day:
:return str: :return str:
""" """
if day.month < 10:
mon = "0" + str(day.month)
else:
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)
day = str(day.year) + "-" + str(day.month) + "-" + tag day = str(day.year) + "-" + mon + "-" + tag
return day return day
@scheduler.task('cron', id="refreshMeals", hour='8-11', day_of_week='*', minute='15', week='*', second='30') async def refreshMeals():
def refreshMeals():
""" """
Aktualisiert immer vormittags 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")
with scheduler.app.app_context():
table = Meals.query.all() table = Meals.query.all()
dates = [] dates = []
for i in table: for i in table:
if i.date not in dates: if i.date not in dates:
dates += [i.date] dates += [i.date]
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 = 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 m in dbmeals: dbnames = []
dbnames += [m.name] for m in dbmeals:
if set (dbnames) != set(apinames) and nomeal not in apinames: dbnames += [m.name]
for n in dbnames: if set(dbnames) != set(apinames) and nomeal not in apinames:
db.session.delete(Meals.query.filter_by(date=i, name=n).first()) for n in dbnames:
db.session.commit() db.session.delete(Meals.query.filter_by(date=i, name=n).first())
getMealsFromAPI(i, True) db.session.commit()
await getMealsFromAPI(i, True)
@scheduler.task('cron', id="mensaschedule", hour='8-11', day_of_week='*', minute='*/15', week='*', second='5')
def mensaschedule():
with app.app_context():
asyncio.run(refreshMeals())

View File

@ -1,9 +1,24 @@
import urllib.error import urllib.error
from urllib.request import urlretrieve from urllib.request import urlretrieve
import asyncio
import httpx
import icalendar import icalendar
import json import json
import recurring_ical_events import recurring_ical_events
from init import scheduler
from init import scheduler, app
async def fetchPlan(session, url):
return await session.get(url=url)
def writeToFile(filename, data):
with open(filename, 'w+') as f:
f.write(data.text)
f.close()
def parseURL(url: str): def parseURL(url: str):
@ -34,7 +49,7 @@ def parseURL(url: str):
return 0 return 0
def getNewRapla(url: str): 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
@ -50,16 +65,18 @@ def getNewRapla(url: str):
try: try:
urlretrieve(url, "calendars/rapla" + kurs + ".ical") urlretrieve(url, "calendars/rapla" + kurs + ".ical")
async with httpx.AsyncClient() as s:
response = await fetchPlan(s, url)
writeToFile(f"calendars/rapla{kurs}.ical", response)
except urllib.error.URLError: except urllib.error.URLError:
return -1 return -1
file = open("calendars/list.json", "r+") file = open("calendars/list.json", "r+")
jsoncal = json.load(file) jsoncal = json.load(file)
jsoncal.update({kurs: ["rapla" + kurs + ".ical", url]}) jsoncal.update({kurs: [f"rapla{kurs}.ical", url]})
file.close() file.close()
file = open("calendars/list.json", "w") file = open("calendars/list.json", "w")
json.dump(jsoncal, file, indent=4) json.dump(jsoncal, file, indent=4)
return "rapla" + kurs + ".ical" return f"rapla{kurs}.ical"
def getIcal(kurs: str): def getIcal(kurs: str):
@ -93,13 +110,23 @@ def getRaplas():
return sorted(kursl), sorted(filel), sorted(urll) return sorted(kursl), sorted(filel), sorted(urll)
@scheduler.task("interval", id="refreshRapla", minutes=30) async def refreshRapla():
def refreshRapla():
""" """
Aktualisiert alle 30 Minuten alle gespeicherten Raplas. Aktualisiert alle 5 Minuten alle gespeicherten Raplas.
""" """
filel = getRaplas()[1] filel = getRaplas()[1]
urll = getRaplas()[2] urll = getRaplas()[2]
for i in range(len(filel)): jobl = []
urlretrieve(urll[i], "calendars/"+filel[i]) async with httpx.AsyncClient() as s:
print("Update die Kalender...") for i in range(len(filel)):
print(f"Update Rapla: {filel[i][:-5]}")
jobl += [fetchPlan(s, urll[i])]
callist = await asyncio.gather(*jobl, return_exceptions=True)
for cal in range(len(callist)):
writeToFile(f"calendars/{filel[cal]}", callist[cal])
@scheduler.task('cron', id="raplaschedule", hour='*', day_of_week='*', minute='*/15', week='*')
def raplaschedule():
with app.app_context():
asyncio.run(refreshRapla())

View File

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
#Erstellt alle benötigten MySQL-Tabellen. #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 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 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));" 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));"

23
init.py
View File

@ -1,7 +1,9 @@
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 talisman import Talisman from flask_talisman import Talisman
from get_mysql import get_mysql from get_mysql import get_mysql
import atexit import atexit
from flask_apscheduler import APScheduler from flask_apscheduler import APScheduler
@ -43,7 +45,6 @@ class User(UserMixin, db.Model):
""" """
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)
password = db.Column(db.String(255))
name = db.Column(db.String(255)) name = db.Column(db.String(255))
kurs = db.Column(db.String(15)) kurs = db.Column(db.String(15))
@ -55,7 +56,17 @@ class Dualis(db.Model):
token = db.Column(db.String(255), unique=True) token = db.Column(db.String(255), unique=True)
uid = db.Column(db.Integer, primary_key=True) uid = db.Column(db.Integer, primary_key=True)
token_created = db.Column(db.Integer) token_created = db.Column(db.Integer)
result_list = db.Column(db.String(15)) semester = db.Column(db.String(15))
class Semesterlist(db.Model):
"""
Datenbank-Modell für Semester-Liste.
"""
uid = db.Column(db.Integer)
semestername = db.Column(db.String(25))
semesterid = db.Column(db.String(15))
itemid = db.Column(db.Integer, primary_key=True)
class Meals(db.Model): class Meals(db.Model):
@ -70,9 +81,11 @@ class Meals(db.Model):
schwein = db.Column(db.Boolean) schwein = db.Column(db.Boolean)
scheduler = APScheduler() scheduler = APScheduler ()
app = create() app = create()
Talisman(app) def_src = ["*.paulmartin.cloud", '\'self\'']
Talisman(app, content_security_policy={"default-src": def_src, "script-src": def_src # + ["'unsafe-inline'"]
})
scheduler.init_app(app) scheduler.init_app(app)
scheduler.start() scheduler.start()
scheduler.api_enabled = True scheduler.api_enabled = True

View File

@ -1,3 +1,6 @@
from init import Semesterlist, User
def getCookie(cookies): def getCookie(cookies):
""" """
Liefert (letzten) Cookie der Cookies-Liste zurück. Liefert (letzten) Cookie der Cookies-Liste zurück.
@ -8,3 +11,16 @@ def getCookie(cookies):
for c in cookies: for c in cookies:
cookie = c.value cookie = c.value
return cookie return cookie
def getSemesterList(uid):
semesterlist = Semesterlist.query.filter_by(uid=uid).all()
semester = []
for s in semesterlist:
semester += [[s.semestername, s.semesterid]]
semester.sort(key=lambda x: x[-1], reverse=True)
return semester
def loadUser(uid):
return User.query.filter_by(id=uid).first()

View File

@ -1,13 +1,17 @@
beautifulsoup4 beautifulsoup4~=4.12.2
Flask Flask~=3.0.3
Flask_APScheduler Flask_APScheduler
Flask_Login Flask_Login
flask_sqlalchemy flask_sqlalchemy
icalendar icalendar~=5.0.11
recurring_ical_events recurring_ical_events
Requests Requests~=2.31.0
talisman talisman
Werkzeug Werkzeug~=3.0.0
lxml lxml
bs4 bs4~=0.0.1
pytz pytz~=2023.3.post1
asyncio~=3.4.3
httpx~=1.0.0b0
celery~=5.4.0rc2

View File

@ -7,10 +7,12 @@ 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
import requesthelpers from requesthelpers import *
from fetchRAPLA import * from fetchRAPLA import *
from calendar_generation import getWeek from calendar_generation import getWeek
from init import * from init import *
@ -22,48 +24,78 @@ def index():
Leitet den normalen Website-Aufruf zum Login weiter. Leitet den normalen Website-Aufruf zum Login weiter.
:return HTML: :return HTML:
""" """
return redirect(url_for("login")) if current_user.is_authenticated:
return welcome()
else:
return login()
@app.route("/welcome")
@app.route("/dashboard", methods=["GET", "POST"])
@login_required @login_required
def welcome(): def welcome():
""" """
Interim Homepage Dashboard
:return HTML: :return HTML:
""" """
if not current_user.kurs:
return redirect(url_for("getKurs", next=url_for(request.endpoint)))
sel = request.args.get("sel")
if not sel:
sel = "theorie"
kurs = current_user.kurs kurs = current_user.kurs
name = current_user.name name = current_user.name
return render_template('index.html', headermessage='DualHub', message="Hallo, " if sel == "theorie":
+ name + " (" + kurs + ")") t = ""
p = "hidden"
else:
t = "hidden"
p = ""
if turbo.can_stream():
print ("Streaming...")
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("/backendpoc/noten") @app.route("/theorie/noten", methods=["GET", "POST"])
@login_required @login_required
def displayNoten(): async def displayNoten():
""" """
Zeigt die Noten aus Dualis an. Hierfür ist ein aktives Token nötig. Zeigt die Noten aus Dualis an. Hierfür ist ein aktives Token nötig.
:return HTML: :return HTML:
""" """
d = Dualis.query.filter_by(uid=current_user.id).first() d = Dualis.query.filter_by(uid=current_user.id).first()
if request.method == "POST":
d.semester = request.form.get("sem")
db.session.commit()
if not d.semester:
return redirect(url_for("getSemester", next=url_for(request.endpoint)))
t = d.token t = d.token
sem = d.result_list chosensemester = d.semester
c = request.cookies.get("cnsc") c = request.cookies.get("cnsc")
timeout = fetchDUALIS.timeOut(d, c, "displayNoten") timeout = fetchDUALIS.timeOut(d, c, "displayNoten")
if timeout: if timeout:
return timeout return timeout
res = fetchDUALIS.getResults(t, c, sem) semester = getSemesterList(current_user.id)
return render_template("noten.html", noten=res, semester=fetchDUALIS.getSem(t, c), sel=sem) noten = await fetchDUALIS.getResults(t, c, chosensemester)
return render_template("noten.html", noten=noten, semester=semester, sel=chosensemester, s="n", praxis="hidden")
@app.route("/backendpoc/plan", methods=["GET"]) @app.route("/plan", methods=["GET"])
@login_required @login_required
def displayRapla(): async def displayRapla():
""" """
Zeigt den Stundenplan für eingeloggte User an. \n Zeigt den Stundenplan für eingeloggte User an. \n
TODO: Persönliche Filter, Notizen, Essensvorlieben etc. berücksichtigen. TODO: Persönliche Filter, Notizen, Essensvorlieben etc. berücksichtigen.
:return HTML: :return HTML:
""" """
if not current_user.kurs:
return redirect(url_for("getKurs", next=url_for(request.endpoint)))
week = request.args.get("week") week = request.args.get("week")
if week: if week:
week = datetime.datetime.strptime(week, "%Y-%m-%d") week = datetime.datetime.strptime(week, "%Y-%m-%d")
@ -72,13 +104,14 @@ def displayRapla():
samstag = request.args.get("samstag") samstag = request.args.get("samstag")
if not samstag: if not samstag:
samstag = False samstag = False
events = getWeek(week, fetchRAPLA.getIcal(current_user.kurs), samstag) events = await getWeek(week, fetchRAPLA.getIcal(current_user.kurs), samstag)
return render_template("plan-user.html", events=events[0], eventdays=events[1], 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]) name=current_user.name, prev=str(events[2])[:10], next=str(events[3])[:10], mon=events[4],
s="p", praxis="hidden")
@app.route("/backendpoc/plan/<string:kurs>") @app.route("/plan/<string:kurs>")
def displayPlan(kurs): async def displayPlan(kurs):
""" """
Zeigt den Stundenplan ohne Login an. \n Zeigt den Stundenplan ohne Login an. \n
Präferenzen werden nicht berücksichtigt. Präferenzen werden nicht berücksichtigt.
@ -101,15 +134,14 @@ def displayPlan(kurs):
samstag = request.args.get("samstag") samstag = request.args.get("samstag")
if not samstag: if not samstag:
samstag = False samstag = False
events = getWeek(week, plan, samstag) events = await getWeek(week, plan, samstag)
print(events)
return render_template("plan-anon.html", events=events[0], eventdays=events[1], kurs=kurs, return render_template("plan-anon.html", events=events[0], eventdays=events[1], kurs=kurs,
prev=str(events[2])[:10], next=str(events[3])[:10], mon=events[4]) prev=str(events[2])[:10], next=str(events[3])[:10], mon=events[4], praxis="hidden")
else: else:
return redirect(url_for("login")) return redirect(url_for("login"))
@app.route("/backendpoc/set-up") @app.route("/set-up")
def redKurs(): def redKurs():
""" """
Setup beginnt mit Kurs. Setup beginnt mit Kurs.
@ -118,9 +150,9 @@ def redKurs():
return redirect(url_for("getKurs")) return redirect(url_for("getKurs"))
@app.route("/backendpoc/set-up/kurs") @app.route("/set-up/kurs")
@login_required @login_required
def getKurs(): async def getKurs():
""" """
Automatische Kurs-Auswahl. \n Automatische Kurs-Auswahl. \n
Aktives Dualis-Token benötigt. Aktives Dualis-Token benötigt.
@ -134,8 +166,11 @@ def getKurs():
return timeout return timeout
e = False e = False
if not current_user.kurs: if not current_user.kurs:
kurs = fetchDUALIS.getKurs(d.token, cookie) kurs = await fetchDUALIS.getKurs(d.token, cookie)
if kurs != 0: if kurs != 0:
if not fetchRAPLA.getIcal(kurs):
return render_template('kurs.html', detected=(kurs, e), s="s", theorie="hidden", praxis="hidden",
file=False)
current_user.kurs = kurs current_user.kurs = kurs
db.session.commit() db.session.commit()
else: else:
@ -147,23 +182,31 @@ def getKurs():
else: else:
e = True e = True
kurs = "" kurs = ""
return render_template('kurs.html', detected=(kurs, e)) return render_template('kurs.html', detected=(kurs, e), s="s", theorie="hidden", praxis="hidden", file=True)
@app.route("/backendpoc/set-up/semester") @app.route("/set-up/semester")
@login_required @login_required
def getSemester(): async def getSemester():
""" """
Manuelle Semester-Auswahl. Manuelle Semester-Auswahl.
:return HTML: :return HTML:
""" """
t = Dualis.query.filter_by(uid=current_user.id).first().token t = Dualis.query.filter_by(uid=current_user.id).first().token
c = request.cookies.get("cnsc") c = request.cookies.get("cnsc")
semesterlist = Semesterlist.query.filter_by(uid=current_user.id).all()
return render_template("semester.html", semester=fetchDUALIS.getSem(t, c)) 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()
else:
semester = getSemesterList(current_user.id)
return render_template("semester.html", semester=semester, s="s", theorie="hidden", praxis="hidden")
@app.route("/backendpoc/set-up/semester", methods=["POST"]) @app.route("/set-up/semester", methods=["POST"])
@login_required @login_required
def setSemester(): def setSemester():
""" """
@ -174,12 +217,12 @@ def setSemester():
if not n: if not n:
n = url_for("welcome") n = url_for("welcome")
d = Dualis.query.filter_by(uid=current_user.id).first() d = Dualis.query.filter_by(uid=current_user.id).first()
d.result_list = request.form.get("sem") d.semester = request.form.get("sem")
db.session.commit() db.session.commit()
return redirect(n) return redirect(n)
@app.route("/backendpoc/set-up/rapla") @app.route("/set-up/rapla")
@login_required @login_required
def chooseRaplas(): def chooseRaplas():
""" """
@ -187,12 +230,12 @@ def chooseRaplas():
:return HTML: :return HTML:
""" """
r = getRaplas() r = getRaplas()
return render_template("rapla.html", raplas=r) return render_template("rapla.html", raplas=r, s="s", theorie="hidden", praxis="hidden")
@app.route("/backendpoc/set-up/rapla", methods=["POST"]) @app.route("/set-up/rapla", methods=["POST"])
@login_required @login_required
def getRapla(): async def getRapla():
""" """
Verarbeitet die Eingabe von chooseRaplas(). Verarbeitet die Eingabe von chooseRaplas().
:return HTML: :return HTML:
@ -202,29 +245,29 @@ def getRapla():
if file == url == "None": if file == url == "None":
return redirect(url_for("chooseRaplas")) return redirect(url_for("chooseRaplas"))
if file != "None": if file != "None":
User.query.filter_by(id=current_user.id).first().kurs = file[5:-5] loadUser(current_user.id).kurs = file[5:-5]
db.session.commit() db.session.commit()
elif url != "None": elif url != "None":
file = getNewRapla(url) file = await getNewRapla(url)
if type(file) is not int: if type(file) is not int:
User.query.filter_by(id=current_user.id).first().kurs = file[5:-5] User.query.filter_by(id=current_user.id).first().kurs = file[5:-5]
db.session.commit() db.session.commit()
else: else:
return redirect(url_for("error", ecode=file + 900)) return redirect(url_for("error", ecode=900))
return redirect(url_for("welcome")) return redirect(url_for("welcome"))
@app.route("/backendpoc/log-in") @app.route("/log-in")
def login(): def login():
""" """
Login-Maske. Login-Maske.
:return HTML: :return HTML:
""" """
return render_template("login.html") return render_template("login.html", theorie="hidden", praxis="hidden", s="s")
@app.route("/backendpoc/log-in", methods=["POST"]) @app.route("/log-in", methods=["POST"])
def login_post(): async def login_post():
""" """
Verarbeitet die Eingabe von login(). \n Verarbeitet die Eingabe von login(). \n
Falls der User schon angelegt ist, wird das Passwort verglichen. \n Falls der User schon angelegt ist, wird das Passwort verglichen. \n
@ -240,67 +283,51 @@ def login_post():
success = make_response(redirect(url_for("getKurs"))) success = make_response(redirect(url_for("getKurs")))
user = User.query.filter_by(email=email).first() user = User.query.filter_by(email=email).first()
newcookie = "" t = await fetchDUALIS.checkUser(email, password)
if user:
dualis = Dualis.query.filter_by(uid=user.id).first()
if check_password_hash(user.password, password):
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)
dualis.token_created = time.time()
db.session.commit()
else:
t = fetchDUALIS.checkUser(email, password)
if t[0] == -2:
return redirect(url_for("login", code=-2))
else:
user.password = generate_password_hash(password, method="pbkdf2:sha256")
dualis.token = t[0]
newcookie = requesthelpers.getCookie(t[1].cookies)
dualis.token_created = time.time()
db.session.commit()
login_user(user)
if user.kurs:
if not n:
success = make_response(redirect(url_for("welcome")))
success.set_cookie("cnsc", value=newcookie, httponly=True, secure=True)
return success
t = fetchDUALIS.checkUser(email, password)
if t[0] == -2: if t[0] == -2:
return redirect(url_for("login", code=-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)
hashid = int(hashlib.sha1(email.encode("utf-8")).hexdigest(), 16) % (10 ** 8) else:
hashpw = generate_password_hash(password, method="pbkdf2:sha256") hashid = int(hashlib.sha1(email.encode("utf-8")).hexdigest(), 16) % (10 ** 8)
pname = email.find(".") + 1 pname = email.find(".") + 1
ename = min(email[pname:].find("."), email[pname:].find("@")) ename = min(email[pname:].find("."), email[pname:].find("@"))
name = email[pname:pname + ename].capitalize() name = email[pname:pname + ename].capitalize()
new_user = User(email=email, password=hashpw, name=name, id=hashid) new_user = User(email=email, name=name, id=hashid)
db.session.add(new_user) db.session.add(new_user)
cookie = requesthelpers.getCookie(t[1].cookies) cookie = requesthelpers.getCookie(t[1].cookies)
new_dualis = Dualis(uid=hashid, token=t[0], token_created=int(time.time())) new_dualis = Dualis(uid=hashid, token=t[0], token_created=int(time.time()))
db.session.add(new_dualis) db.session.add(new_dualis)
db.session.commit() db.session.commit()
login_user(new_user) login_user(new_user)
newcookie = cookie success.set_cookie("cnsc", value=cookie, httponly=True, secure=True)
success.set_cookie("cnsc", value=newcookie, httponly=True, secure=True)
return success return success
@app.route("/backendpoc/log-out") @app.route("/log-out")
@login_required @login_required
def logout(): async def logout():
""" """
Loggt den User aus. Loggt den User aus.
:return Empty Token: :return Empty Token:
""" """
cookie = request.cookies.get("cnsc") cookie = request.cookies.get("cnsc")
dualis = Dualis.query.filter_by(uid=current_user.id).first() dualis = Dualis.query.filter_by(uid=current_user.id).first()
fetchDUALIS.logOut(dualis.token, cookie) await fetchDUALIS.logOut(dualis.token, cookie)
dualis.token = None dualis.token = None
db.session.commit() db.session.commit()
logout_user() logout_user()
@ -311,29 +338,30 @@ def logout():
return red return red
@app.route("/backendpoc/error<int:ecode>") @app.route("/error")
def error(ecode): def error():
""" """
Error Page für custom-Errors. \n Error Page für custom-Errors. \n
TODO: Funktion depreciaten. Ersetzen durch Errors auf den entsprechenden Seiten. TODO: Funktion depreciaten. Ersetzen durch Errors auf den entsprechenden Seiten.
:param ecode:
:return: :return:
""" """
if ecode == 900: error = request.args.get("ecode")
if error == "900":
msg = "Ungültige RAPLA-URL! Sicher, dass der Link zum DHBW-Rapla führt?" msg = "Ungültige RAPLA-URL! Sicher, dass der Link zum DHBW-Rapla führt?"
elif ecode == 899: elif error == "899":
msg = "Der Kalender wurde nicht gefunden! Sicher, dass der Link korrekt ist?" msg = "Der Kalender wurde nicht gefunden! Sicher, dass der Link korrekt ist?"
else: else:
msg = "Unbekannter Fehler!" msg = str(error)
return render_template('index.html', message=msg, headermessage="DualHub") return render_template('display-message.html', message=msg)
@app.route("/error")
@app.errorhandler(HTTPException) @app.errorhandler(HTTPException)
def handle(e): def handle(e):
"""" """"
HTTP-Exception-Handler HTTP-Exception-Handler
""" """
return render_template('index.html', message=e, headermessage="DualHub") return render_template('display-message.html', message=e)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -6,12 +6,12 @@
--timeHeight: 60px; --timeHeight: 60px;
--halfTimeHeight: 30px; --halfTimeHeight: 30px;
--quartTimeHeight: 15px; --quartTimeHeight: 15px;
--calBgColor: #fff1f8; --calBgColor: #3f2d2d;
--eventBorderColor: #f2d3d8; --eventBorderColor: #000000;
--eventColor1: #ffd6d1; --eventColor1: #b82424;
--eventColor2: #fafaa3; --eventColor2: #fafaa3;
--eventColor3: #e2f8ff; --eventColor3: #1894bb;
--eventColor4: #d1ffe6; --eventColor4: rgba(184, 36, 36, 0.65);
} }
.calendar { .calendar {
@ -22,16 +22,17 @@
} }
select { select {
width: 300px; width: 100%;
height: 40px; height: 40px;
border-radius: 5px; border-radius: 5px;
color: white;
border-color: transparent; border-color: transparent;
background-color: var(--eventColor4); background-color: var(--calBgColor);
text-align: center; text-align: center;
} }
.userbuttons { .userbuttons {
width: 90%; width: 95%;
position: absolute; position: absolute;
display: flex; display: flex;
justify-content: end; justify-content: end;
@ -39,15 +40,15 @@ select {
.changeweek { .changeweek {
font-size: 2.5em; font-size: 2.5em;
color: black;
text-decoration: none; text-decoration: none;
font-weight: 600; font-weight: 600;
color: white;
} }
.changeweek:hover { .changeweek:hover {
color: transparent; color: transparent;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
-webkit-text-stroke: 2px black; -webkit-text-stroke: 3px white;
} }
h2 { h2 {
@ -69,7 +70,8 @@ h2 {
button { button {
border-radius: 5px; border-radius: 5px;
border-color: black; border-color: white;
color: white;
border-style: solid; border-style: solid;
background: transparent; background: transparent;
font-weight: 600; font-weight: 600;
@ -112,6 +114,7 @@ option {
display: grid; display: grid;
grid-template-rows: repeat(var(--numQuartHours), var(--quartTimeHeight)); grid-template-rows: repeat(var(--numQuartHours), var(--quartTimeHeight));
border-radius: 5px; border-radius: 5px;
color: black;
background: var(--calBgColor); background: var(--calBgColor);
} }
@ -138,17 +141,24 @@ option {
border-radius: 5px; border-radius: 5px;
padding: 0.5rem; padding: 0.5rem;
margin: 0 0.5rem; margin: 0 0.5rem;
background: white; color: white;
background: var(--eventColor4);
position: relative; position: relative;
} }
.event:hover {
transform: scale(102%);
}
.space, .space,
.date { .date {
height: 100px; height: 100px;
} }
body { body {
font-family: system-ui, sans-serif; font-family: "Asap", "Calibri", "Arial", sans-serif;
color: white;
background-color: black;
} }
.date { .date {
@ -168,6 +178,93 @@ body {
font-weight: 100; font-weight: 100;
} }
.cs1 {
margin-left: 220px;
padding: 20px;
}
nav ul {
list-style-type: none;
margin: 0;
padding: 0;
width: 200px;
background-color: #333;
position: fixed;
top: 0;
left: 0;
height: 100%;
overflow-y: auto;
font-size: 150%;
}
nav li {
border-top: none;
}
nav li.top {
border-style: solid;
border-radius: 5px;
border-color: white;
margin: 10px;
text-align: center;
}
nav a.top:hover {
background: transparent;
}
nav li.start a {
border-top: 3px solid white;
margin-top: 20px;
padding-top: 30px;
}
nav a.selected {
color: red;
background-color: black;
}
nav li.top.selected {
border-color: red;
}
nav li a {
display: block;
color: white;
padding: 12px 16px;
text-decoration: none;
}
nav li a:hover {
background-color: red;
color: black;
}
nav li .bottom {
position: absolute;
bottom: 0;
width: 200px;
}
.space {
width: 1%;
}
.footerdiv {
width: 100%;
justify-content: right;
display: flex;
}
a.footer {
color: white;
text-decoration: none;
}
a.footer:hover {
font-weight: 600;
}
.start-0100 { .start-0100 {
grid-row-start: 1; grid-row-start: 1;
} }

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;;;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;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAMF;EACE;EACA;EACA;EACA;;;AAGF;AAAA;EAEE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAAA;EAEE;;;AAKF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;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

@ -6,12 +6,12 @@
--timeHeight: 60px; --timeHeight: 60px;
--halfTimeHeight: 30px; --halfTimeHeight: 30px;
--quartTimeHeight: 15px; --quartTimeHeight: 15px;
--calBgColor: #fff1f8; --calBgColor: #3f2d2d;
--eventBorderColor: #f2d3d8; --eventBorderColor: #000000;
--eventColor1: #ffd6d1; --eventColor1: #b82424;
--eventColor2: #fafaa3; --eventColor2: #fafaa3;
--eventColor3: #e2f8ff; --eventColor3: #1894bb;
--eventColor4: #d1ffe6; --eventColor4: rgba(184, 36, 36, 0.65);
} }
.calendar { .calendar {
@ -22,16 +22,17 @@
} }
select { select {
width: 300px; width: 100%;
height: 40px; height: 40px;
border-radius: 5px; border-radius: 5px;
color: white;
border-color: transparent; border-color: transparent;
background-color: var(--eventColor4); background-color: var(--calBgColor);
text-align: center; text-align: center;
} }
.userbuttons { .userbuttons {
width: 90%; width: 95%;
position: absolute; position: absolute;
display: flex; display: flex;
justify-content: end; justify-content: end;
@ -39,15 +40,15 @@ select {
.changeweek { .changeweek {
font-size: 2.5em; font-size: 2.5em;
color: black;
text-decoration: none; text-decoration: none;
font-weight: 600; font-weight: 600;
color: white;
} }
.changeweek:hover { .changeweek:hover {
color: transparent; color: transparent;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
-webkit-text-stroke: 2px black; -webkit-text-stroke: 3px white;
} }
h2 { h2 {
@ -69,7 +70,8 @@ h2 {
button { button {
border-radius: 5px; border-radius: 5px;
border-color: black; border-color: white;
color: white;
border-style: solid; border-style: solid;
background: transparent; background: transparent;
font-weight: 600; font-weight: 600;
@ -111,6 +113,7 @@ option {
display: grid; display: grid;
grid-template-rows: repeat(var(--numQuartHours), var(--quartTimeHeight)); grid-template-rows: repeat(var(--numQuartHours), var(--quartTimeHeight));
border-radius: 5px; border-radius: 5px;
color: black;
background: var(--calBgColor); background: var(--calBgColor);
} }
@ -140,10 +143,15 @@ option {
border-radius: 5px; border-radius: 5px;
padding: 0.5rem; padding: 0.5rem;
margin: 0 0.5rem; margin: 0 0.5rem;
background: white; color: white;
background: var(--eventColor4);
position: relative; position: relative;
} }
.event:hover{
transform: scale(102%);
}
.space, .space,
.date { .date {
height: 100px height: 100px
@ -152,7 +160,9 @@ option {
// Global / Etc // Global / Etc
body { body {
font-family: system-ui, sans-serif; font-family: "Asap", "Calibri", "Arial", sans-serif;
color: white;
background-color: black;
} }
.date { .date {
@ -172,6 +182,97 @@ body {
font-weight: 100; font-weight: 100;
} }
// Navbar
.cs1 {
margin-left: 220px;
padding: 20px;
}
nav ul {
list-style-type: none;
margin: 0;
padding: 0;
width: 200px;
background-color: #333;
position: fixed;
top: 0;
left: 0;
height: 100%;
overflow-y: auto;
font-size: 150%;
}
nav li {
border-top: none;
}
nav li.top {
border-style: solid;
border-radius: 5px;
border-color: white;
margin: 10px;
text-align: center;
}
nav a.top:hover {
background: transparent;
}
nav li.start a {
border-top: 3px solid white;
margin-top: 20px;
padding-top: 30px;
}
nav a.selected {
color: red;
background-color: black
}
nav li.top.selected {
border-color: red;
}
nav li a {
display: block;
color: white;
padding: 12px 16px;
text-decoration: none;
}
nav li a:hover {
background-color: red;
color: black;
}
nav li .bottom {
position: absolute;
bottom: 0;
width: 200px;
}
.space {
width: 1%;
}
.footerdiv {
width: 100%;
justify-content: right;
display: flex;
}
a.footer {
color: white;
text-decoration: none;
}
a.footer:hover {
font-weight: 600;
}
// Place on Timeline // Place on Timeline

7
static/dropdown.js Normal file
View File

@ -0,0 +1,7 @@
document.addEventListener("DOMContentLoaded", function(event) {
const dropdown = document.getElementById("dropdown");
dropdown.addEventListener("change", function() {
dropdown.submit();
});
});

View File

@ -1,107 +1,164 @@
body :root {
{ --numBoxes: 2;
background-color: #1d1919; --numRows: 2;
font-size: 150%; }
font-family: "Asap","Calibri", "Arial";
color: #fff1f1; body {
animation-name: HG; background-color: black;
animation-duration: 20s; font-family: "Asap", "Calibri", "Arial", sans-serif;
animation-iteration-count: infinite font-size: 150%;
} margin: 0;
color: white;
input { }
width: 150px;
height: 50px nav ul {
} list-style-type: none;
margin: 0;
input[type=url], input[type=email], input[type=password] { padding: 0;
width: 500px; width: 200px;
} background-color: #333;
position: fixed;
select { top: 0;
width: 200px; left: 0;
height: 50px height: 100%;
} overflow-y: auto;
}
.cs
{ nav li {
border-top: none;
position: absolute; }
top: 38%;
left:12%; nav li.top {
margin: 0; border-style: solid;
width: 75%; border-radius: 5px;
/*border: 3px solid red;*/ border-color: white;
} margin: 10px;
text-align: center;
.cs h1, .cs h2, .cs form }
{
display: flex; nav a.top:hover {
text-align: center; background: transparent;
display: flex; }
justify-content: center;
align-items: center; nav li.start a {
height: 200%; border-top: 3px solid white;
/*border: 3px solid green;*/ margin-top: 20px;
} padding-top: 30px;
h1 }
{
text-align: center; nav a.selected {
font-weight: bold; color: red;
font-family: "Asap","Calibri","Arial"; }
font-size: 300%;
color: #e7dbdb; nav li.top.selected {
} border-color: red;
}
h2
{ nav li a.selected {
font-size: 100%; background-color: black;
position: sticky; }
bottom: 0;
text-align: left; nav li a {
} display: block;
color: white;
p padding: 12px 16px;
{ text-decoration: none;
line-height: 150%; }
}
nav li a:hover {
a:link background-color: red;
{ color: black;
font-style: italic; }
text-decoration: none;
font-weight: bolder; nav li .bottom {
color: #adadad position: absolute;
} bottom: 0;
width: 200px;
a:visited }
{
color: #9e9393; .cs1 {
} margin-left: 220px;
padding: 20px;
a:hover }
{
color: #726868; .cs {
font-size: 105%; display: flex;
transition: font-size 100ms ease-in; justify-content: center;
} align-items: center;
flex-direction: column;
a, img padding: 20px;
{ }
transition: 100ms ease-out
} .container {
display: flex;
img flex-direction: column;
{ }
width: 30%;
height: auto /* Reihen */
} .row {
display: flex;
img:hover justify-content: space-evenly;
{ margin-bottom: 10px;
width: 32%; }
height: auto;
opacity: 75%; .box {
transition: 100ms ease-in; width: calc(100% / var(--numBoxes));
} height: 300px;
border: 5px solid red;
margin: 7px;
border-radius: 10px;
}
.box:hover {
transform: scale(101%);
}
.cs1 h1 {
margin-left: 0;
}
.notification-icon {
position: fixed;
top: 10px;
right: 10px;
width: 50px;
}
.space {
width: 1%;
}
.footerdiv {
position: absolute;
width: 100%;
justify-content: right;
bottom: 10px;
right: 10px;
font-size: 80%;
display: flex;
}
a.footer {
color: white;
text-decoration: none;
}
a.footer:hover {
font-weight: 600;
}
input {
width: 150px;
height: 50px;
}
input[type=url], input[type=email], input[type=password] {
width: 500px;
}
select {
width: 200px;
height: 50px;
}
/*# sourceMappingURL=style.css.map */

1
static/style.css.map Normal file
View File

@ -0,0 +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;;;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"}

163
static/style.scss Normal file
View File

@ -0,0 +1,163 @@
:root {
--numBoxes: 2;
--numRows: 2;
}
body {
background-color: black;
font-family: "Asap", "Calibri", "Arial", sans-serif;
font-size: 150%;
margin: 0;
color: white;
}
nav ul {
list-style-type: none;
margin: 0;
padding: 0;
width: 200px;
background-color: #333;
position: fixed;
top: 0;
left: 0;
height: 100%;
overflow-y: auto;
}
nav li {
border-top: none;
}
nav li.top {
border-style: solid;
border-radius: 5px;
border-color: white;
margin: 10px;
text-align: center;
}
nav a.top:hover {
background: transparent;
}
nav li.start a {
border-top: 3px solid white;
margin-top: 20px;
padding-top: 30px;
}
nav a.selected {
color: red;
}
nav li.top.selected {
border-color: red;
}
nav li a.selected {
background-color: black;
}
nav li a {
display: block;
color: white;
padding: 12px 16px;
text-decoration: none;
}
nav li a:hover {
background-color: red;
color: black;
}
nav li .bottom {
position: absolute;
bottom: 0;
width: 200px;
}
.cs1 {
margin-left: 220px;
padding: 20px;
}
.cs {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 20px;
}
.container {
display: flex;
flex-direction: column;
}
/* Reihen */
.row {
display: flex;
justify-content: space-evenly;
margin-bottom: 10px;
}
.box {
width: calc(100%/var(--numBoxes));
height: 300px;
border: 5px solid red;
margin: 7px;
border-radius: 10px;
}
.box:hover {
transform: scale(101%);
}
.cs1 h1 {
margin-left: 0;
}
.notification-icon {
position: fixed;
top: 10px;
right: 10px;
width:50px;
}
.space {
width: 1%;
}
.footerdiv {
position: absolute;
width: 100%;
justify-content: right;
bottom: 10px;
right: 10px;
font-size: 80%;
display: flex;
}
a.footer {
color: white;
text-decoration: none;
}
a.footer:hover {
font-weight: 600;
}
input {
width: 150px;
height: 50px
}
input[type=url], input[type=email], input[type=password] {
width: 500px;
}
select {
width: 200px;
height: 50px
}

57
templates/dashboard.html Normal file
View File

@ -0,0 +1,57 @@
{% extends "index.html" %}
{% block content %}
<div class="cs">
<h1> Willkommen, {{ name }} ({{ kurs }})!</h1>
</div>
{% if praxis == "hidden" %}
<div class="container">
<div class="row">
<div class="box">
<div class="cs">
<h2>Aktueller GPA</h2>
</div>
</div>
<div class="box">
<div class="cs">
<h2>Countdown bis zur nächsten Klausur</h2>
</div>
</div>
</div>
<div class="row">
<div class="box">
<div class="cs">
<h2>Lernplan</h2>
</div>
</div>
<div class="box">
<div class="cs">
<h2>Nächste Vorlesung</h2>
</div>
</div>
<div class="box">
<div class="cs">
<h2>Neueste Chatnachricht</h2>
</div>
</div>
</div>
</div>
{% else %}
<div class="container">
<div class="row">
<div class="box">
<div class="cs">
<h2>ToDos</h2>
</div>
</div>
<div class="box">
<div class="cs">
<h2>Literatursammlung</h2>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,4 @@
{% extends "index.html" %}
{% block content %}
<h2>{{ message }}</h2>
{% endblock %}

View File

@ -1,16 +1,42 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<title> {{headermessage}} 👀</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">
<script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script>
{{ turbo() }}
{% block head %}
{% endblock %}
</head>
<body>
<nav>
<ul id = "navbar-items">
{% if theorie=="hidden" and praxis=="hidden" %}
<li class="top"><a class="top" href={{ url_for("index", sel="theorie") }}>Theorie</a></li>
<li class="top"><a class="top" href={{ url_for("index", sel="praxis") }}>Praxis</a></li>
{% elif theorie == "hidden" %}
<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("index", sel="praxis") }}>Praxis</a></li>
{% else %}
<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("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>
<li {% block logout %}{% endblock %}><a class="bottom" href={{ url_for("logout") }}>Log-Out</a></li>
</ul>
</nav>
<script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script> <turbo-frame>
<div class="cs1" id = 'page'>
<div id="content">
</head> {% block content %}
<body> {% endblock %}
<div class="cs"> </div>
<h1>{{message}}</h1> </div>
</div> </turbo-frame>
</body> </body>
</html> </html>

View File

@ -1,22 +1,22 @@
<!DOCTYPE html> {% extends "index.html" %}
<html lang="en"> {% block content %}
<head>
<meta charset="UTF-8">
<title>Kurs wählen</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
<script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script>
</head>
<body>
{% if not detected[1] %} {% if not detected[1] %}
<h1>Wir haben {{ detected[0] }} als deinen Kurs ermittelt. Falls er nicht stimmt, kannst du ihn unten auswählen.</h1> {% if file %}
<form action={{ url_for("getSemester") }}> <h1>Wir haben {{ detected[0] }} als deinen Kurs ermittelt. Falls er nicht stimmt, kannst du ihn unten auswählen.</h1>
<input type="submit" value="Der Kurs stimmt!"> {% if not request.args.get("next") %}
</form> <form action={{ url_for("getSemester") }}>
{% else %}
<form action={{ request.args.get("next") }}>
{% endif %}
<input type="submit" value="Der Kurs stimmt!">
</form>
{% else %}
<h1>Wir haben {{ detected[0] }} als deinen Kurs ermittelt. Für ihn ist aber noch kein Rapla hinterlegt.</h1>
{% endif %}
{% else %} {% else %}
<h1>Dein Kurs konnte leider nicht ermittelt werden. Klicke den Button, um direkt 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 action={{ url_for("chooseRaplas") }}> <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>
</body> {% endblock %}
</html>

View File

@ -1,22 +1,19 @@
<!DOCTYPE html> {% extends "index.html" %}
<html lang="de"> {% block logout %}
<head> hidden
<title> Log In 👀</title> {% endblock %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
<script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script> {% block content %}
<div class="cs">
<h1>Einloggen</h1>
</head> </div>
<body> <div class="cs">
<div class="cs"> <form method="post" action={{ url_for ("login_post", next=request.args.get("next")) }}>
<form method="post" action={{ url_for ("login_post", next=request.args.get("next")) }}> <label for="email" >Dualis-Email: </label>
<label for="email" >Dualis-Email: </label> <input class="input" type="email" name="email" placeholder="Email-Adresse" autofocus="">
<input class="input" type="email" name="email" placeholder="Email-Adresse" autofocus=""> <label for="password">Dualis-Passwort:</label>
<label for="password">Dualis-Passwort:</label> <input class="input" type="password" name="password" placeholder="Passwort">
<input class="input" type="password" name="password" placeholder="Passwort"> <button class="button">Login</button>
<button class="button">Login</button> </form>
</form> </div>
</div> {% endblock %}
</body>
</html>

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

@ -1,23 +1,22 @@
<!DOCTYPE html> {% extends "index.html" %}
<html lang="en"> {% block head %}
<head> <script type="text/javascript" src="{{ url_for("static", filename="dropdown.js") }}"></script>
<meta charset="UTF-8"> <meta http-equiv="Refresh" content="600">
<title>Noten</title> {% endblock %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> {% block content %}
<script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script> <turbo-frame>
</head> <div id="noten">
<body> {% include "noten-list.html" %}
{% for i in noten %} </div>
<h2>{{ i[0] }}: {{ i[1].capitalize() }} (Credits: {{ i[2] }})</h2> </turbo-frame>
{% endfor %} <br>
<form method="post" action={{ url_for ("setSemester", next=url_for("displayNoten")) }}> <br>
<form id= "dropdown" method="post" action={{ url_for ("displayNoten") }}>
<label for="sem">Semester wählen! </label> <label for="sem">Semester wählen! </label>
<select name="sem" id="sem"> <select name="sem" id="sem">
{% for i in range (semester|length) %} {% for i in range (semester|length) %}
<option value= {{ semester [i] [1] }} {% if semester[i][1]==sel %} selected {% endif %}>{{ semester [i] [0] }} </option> <option value= {{ semester [i] [1] }} {% if semester[i][1]==sel %} selected {% endif %}>{{ semester [i] [0] }} </option>
{% endfor %} {% endfor %}
</select> </select>
<input type="submit" value="Semester setzen!">
</form> </form>
</body> {% endblock %}
</html>

View File

@ -1,7 +1,9 @@
{% extends "plan-general.html"%} {% extends "plan-general.html"%}
{% block loginout %}
<li><a class="bottom" href={{ url_for("login") }}>Log-In</a></li>
{% endblock %}
{% block startcontent %} {% block startcontent %}
<h2>Vorlesungsplan {{ kurs }}</h2> <h2>Vorlesungsplan {{ kurs }}</h2>
{% endblock %} {% endblock %}
{% block endcontent %} {% block endcontent %}
<a href={{ url_for("login") }}>Einloggen, um alle Features zu nutzen!</a>
{% endblock %} {% endblock %}

View File

@ -4,8 +4,22 @@
<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 async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script>
</head> </head>
<body> <body>
<nav>
<ul>
<li class="top selected"><a class="top selected" href={{ url_for("welcome", sel="theorie") }}>Theorie</a></li>
<li class="top"><a class="top" href={{ url_for("welcome", sel="praxis") }}>Praxis</a></li>
<li {{ theorie }}><a href={{ url_for("displayNoten") }}>Noten</a></li>
<li {{ theorie }}><a class="selected" href={{ url_for("displayRapla") }}>Stundenplan</a></li>
<li {{ praxis }}><a href={{ url_for("redKurs") }}>To Dos</a></li>
<li><a href={{ url_for("redKurs") }}>Konfiguration</a></li>
{% block loginout %}
{% endblock %}
</ul>
</nav>
<div class="cs1">
<div class="customheader"> <div class="customheader">
{% block startcontent %} {% block startcontent %}
{% endblock %} {% endblock %}
@ -64,5 +78,6 @@
</div> </div>
{% block endcontent %} {% block endcontent %}
{% endblock %} {% endblock %}
</div>
</body> </body>
</html> </html>

View File

@ -1,4 +1,7 @@
{% extends "plan-general.html" %} {% extends "plan-general.html" %}
{% block loginout %}
<li><a class="bottom" href={{ url_for("logout") }}>Log-Out</a></li>
{% endblock %}
{% block startcontent %} {% block startcontent %}
<h2>{{ name }}s Vorlesungsplan</h2> <h2>{{ name }}s Vorlesungsplan</h2>
{% endblock %} {% endblock %}
@ -9,5 +12,4 @@
</div> </div>
{% endblock %} {% endblock %}
{% block endcontent %} {% block endcontent %}
<!-- Button zum Hinzufügen von Kommentaren -->
{% endblock %} {% endblock %}

View File

@ -1,11 +1,5 @@
<!DOCTYPE html> {% extends "index.html" %}
<html lang="de"> {% block content %}
<head>
<title> RAPLA importieren 👀</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
<script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script>
</head>
<body>
<h1>Verfügbare Raplas </h1> <h1>Verfügbare Raplas </h1>
<form 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>
@ -20,9 +14,8 @@
</form> </form>
<h1>Eigenen Rapla hinzufügen</h1> <h1>Eigenen Rapla hinzufügen</h1>
<form 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!">
</form> </form>
</body> {% endblock %}
</html>

View File

@ -1,14 +1,10 @@
<!DOCTYPE html> {% extends "index.html" %}
<html lang="en"> {% block head %}
<head> <script type="text/javascript" src="{{ url_for("static", filename="dropdown.js") }}"></script>
<meta charset="UTF-8"> {% endblock %}
<title>Semester wählen</title> {% block content %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
<script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script>
</head>
<body>
<h1>Bitte wähle aus der unten stehenden Liste das Semester, dessen Noten du sehen möchtest.</h1> <h1>Bitte wähle aus der unten stehenden Liste das Semester, dessen Noten du sehen möchtest.</h1>
<form method="post" action={{ url_for ("setSemester") }}> <form id="dropdown" method="post" action={{ url_for ("setSemester", next=request.args.get("next")) }}>
<label for="sem">Semester wählen! </label> <label for="sem">Semester wählen! </label>
<select name="sem" id="sem"> <select name="sem" id="sem">
<option value = "none" selected disabled hidden></option> <option value = "none" selected disabled hidden></option>
@ -16,7 +12,5 @@
<option value= {{ semester [i] [1] }}>{{ semester [i] [0] }} </option> <option value= {{ semester [i] [1] }}>{{ semester [i] [0] }} </option>
{% endfor %} {% endfor %}
</select> </select>
<input type="submit" value="Semester setzen!">
</form> </form>
</body> {% endblock %}
</html>

284
tests_examples/vergleich.py Normal file
View File

@ -0,0 +1,284 @@
import asyncio
import httpx
import time
import urllib.parse
import requests
from flask import Flask
from bs4 import BeautifulSoup
from celery import Celery
import fetchDUALIS
from login_data import passwort, email #CREATE LOCAL login_data.py!!!
app = Flask(__name__)
app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0'
app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:6379/0'
celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
celery.conf.update(app.config)
headers = {
'Cookie': 'cnsc=0',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/58.0.3029.110 Safari/537.36',
'Accept-Encoding': 'utf-8'
}
url = "https://dualis.dhbw.de/scripts/mgrqispi.dll"
fpw = urllib.parse.quote(passwort, safe='', encoding=None, errors=None)
fmail = urllib.parse.quote(email, safe='', encoding=None, errors=None)
@celery.task(bind=True)
def celery_requests(self):
with httpx.Client() as s:
content = (f'usrname={fmail}&pass={fpw}&ARGUMENTS=clino%2Cusrname%2Cpass%2Cmenuno%2Cmenu_type%2Cbrowser'
f'%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK')
response = s.post(url=url, headers=headers, content=content)
return response, s
# noinspection DuplicatedCode
async def checkUser_celery():
req = celery_requests.apply_async()
response = req[0]
s = req[1]
header = response.headers
try:
refresh = header["REFRESH"]
arg = refresh.find("=-N") + 3
komma = refresh[arg:].find(",")
cookie = s.cookies.get("cnsc")
except KeyError:
return -2, s
token = refresh[arg:komma + arg]
return token, cookie
# noinspection DuplicatedCode
def checkUser_normal():
"""
Erhält von Dualis den Token und Cookie für User.
:param email:
:param password:
:return (Token, Session):
"""
s = requests.Session()
payload = 'usrname=' + fmail + '&pass=' + fpw + ('&ARGUMENTS=clino%2Cusrname%2Cpass%2Cmenuno%2Cmenu_type%2Cbrowser'
'%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK')
response = s.post(url, headers=headers, data=payload)
header = response.headers
try:
refresh = header["REFRESH"]
arg = refresh.find("=-N") + 3
komma = refresh[arg:].find(",")
except KeyError:
return -2, s
token = refresh[arg:komma + arg]
cookies = s.cookies
cookie = 0
for c in cookies:
cookie = c.value
return token, cookie
# noinspection DuplicatedCode
async def checkUser_async():
"""
Erhält von Dualis den Token und Cookie für User.
:param email:
:param password:
:return (Token, Session):
"""
# noinspection DuplicatedCode
async with httpx.AsyncClient() as s:
content = (f'usrname={fmail}&pass={fpw}&ARGUMENTS=clino%2Cusrname%2Cpass%2Cmenuno%2Cmenu_type%2Cbrowser'
f'%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK')
response = await s.post(url=url, headers=headers, content=content)
header = response.headers
try:
refresh = header["REFRESH"]
arg = refresh.find("=-N") + 3
komma = refresh[arg:].find(",")
cookie = s.cookies.get("cnsc")
except KeyError:
return -2, s
token = refresh[arg:komma + arg]
return token, cookie
# noinspection DuplicatedCode
def getSem_normal(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 +
"?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N" + token + ",-N000307,",
headers=headers, data={})
html = BeautifulSoup(response.text, 'lxml')
select = html.find('select')
select = select.find_all(value=True)
optlist = []
for i in select:
t = i.text.replace("Wi", "Winter").replace("So", "Sommer")
t = t.replace("Se", "semester")
optlist += [[t, i['value']]]
return optlist
# noinspection DuplicatedCode
async def getSem_async(token, cookie):
"""
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)
async with httpx.AsyncClient() as s:
response = await s.get(url=url +
"?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N" + token + ",-N000307,",
headers=headers)
html = BeautifulSoup(response.text, 'lxml')
select = html.find('select')
select = select.find_all(value=True)
optlist = []
for i in select:
t = i.text.replace("Wi", "Winter").replace("So", "Sommer")
t = t.replace("Se", "semester")
optlist += [[t, i['value']]]
return optlist
async def getSem_celery(token, cookie):
pass
# noinspection DuplicatedCode
async def getResults_async(token, cookie, resl):
headers["Cookie"] = "cnsc=" + cookie
async with httpx.AsyncClient() as s:
response = await s.get(url=url + "?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N" + token +
",-N000307," + ",-N" + resl, headers=headers)
html = BeautifulSoup(response.content.decode("utf-8"), 'lxml')
table = html.find('table', attrs={"class": "nb list"})
body = table.find("tbody")
vorl = body.find_all("tr")
vorlist = []
tasks = []
i = 0
for row in vorl:
cols = row.find_all("td")
col = [[e.text.strip()] for e in cols]
if len(col) != 0:
if len(col[4][0]) == 0:
tasks += [getPruefung_async(s, row.find("a")["href"])]
col[2] = i
i += 1
vorlist += [col[1:4]]
extrakurse = await asyncio.gather(*tasks, return_exceptions=True)
for i in vorlist:
for e in range(0, len(i)):
if isinstance(i[e], int):
i[e] = extrakurse[i[e]]
return vorlist[:-1]
# noinspection DuplicatedCode
async def getPruefung_async(s, url):
response = await s.get("https://dualis.dhbw.de" + url, headers=headers)
html = BeautifulSoup(response.content.decode("utf-8"), 'lxml')
table = html.find('table')
pruefung = table.find_all("tr")
ret = []
for row in pruefung:
cols = row.find_all("td")
col = [e.text.strip() for e in cols]
if len(col) == 6 and len(col[3]) <= 13:
if len(col[3]) != 0:
ret += [col[0] + ": " + col[3][:3]]
if ret[-1][0] == ':':
ret[-1] = "Gesamt" + ret[-1]
if len(ret) == 0:
ret = ["Noch nicht gesetzt"]
return ret
def normal():
login = checkUser_normal()
token = login[0]
cookie = str(login[1])
semlist = getSem_normal(token, cookie)
for i in range(1, len(semlist)):
results = fetchDUALIS.getResults(token, cookie, semlist[i][1])
return [token, cookie, results]
async def async_normal():
login = await checkUser_async()
token = login[0]
cookie = str(login[1])
semlist = await getSem_async(token, cookie)
for i in range(1, len(semlist)):
results = await getResults_async(token, cookie, semlist[i][1])
return [token, cookie, results]
@app.route('/c')
async def celery_normal():
login = await checkUser_celery()
token = login[0]
cookie = str(login[1])
#print(token, cookie)
#semlist = await getSem_celery(token, cookie)
#print(semlist)
#for i in range(0, len(semlist)):
# results = await getResults_celery(token, cookie, semlist[i][1])
# print(results)
return [token, cookie]
@app.route('/')
async def tests():
normaltime = 0
asynctime = 0
iter = 20
print("Starting...")
for i in range(iter):
start = time.perf_counter()
n = normal()
end = time.perf_counter()
normaltimeloop = end - start
await fetchDUALIS.logOut(n[0], n[1])
start = time.perf_counter()
a = await async_normal()
end = time.perf_counter()
asynctimeloop = end - start
await fetchDUALIS.logOut(a[0], a[1])
if a[2] == n[2]:
normaltime += normaltimeloop
asynctime += asynctimeloop
print(str(((i + 1) / iter) * 100) + '%')
#vergl = "Gleicher Inhalt!"
#else:
# vergl = "ACHTUNG! Ungleicher Inhalt!"
#return "<br><h2>" + vergl + "</h2><br> <h1> Normal: " + str(normaltime) + "</h2><br><br> <h1> Async: " + str(asynctime) + "</h1>"
delta = normaltime / iter - asynctime / iter
return f"<br><h2> Durchschnitt normal: {normaltime / iter} </h2><br><h2> Durchschnitt asynchron: {asynctime / iter} </h2><br><h1> Durchschnittliches Delta: {delta} </h1><br>"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1024, debug=True)