31 Commits

Author SHA1 Message Date
342d96b626 Added motd.js 2024-11-19 15:12:31 +01:00
f27d54798c Rapla formatting 2024-10-08 10:00:21 +02:00
34822bfc93 DB refactoring 2024-10-07 15:12:06 +02:00
4c5a0c52d0 Quickfix missing await 2024-10-07 13:13:07 +02:00
662407a136 Update styles, Semesterlist 2024-10-07 12:51:49 +02:00
85c129b602 Custom Calendar 2024-09-29 19:21:20 +02:00
b0bda20baa Server Config 2024-06-18 12:20:42 +02:00
0fb8d0d4bc Update README.md 2024-06-18 09:35:31 +02:00
e044cd8434 Quick Fixes 2024-06-17 12:47:38 +02:00
079e405942 Suppress Cron when testing 2024-06-04 10:28:34 +02:00
0a38bb951b buildICALfromKey Test default 2024-06-04 10:23:45 +02:00
ab67eacd0e Test Rapla-File u. Blackbox 2024-06-03 08:43:49 +02:00
00f1ad007a Test Rapla-File 2024-06-03 08:04:31 +02:00
2027397e03 Whitebox Tests 2024-06-03 11:51:05 +02:00
9fa7cfe867 Refactoring MySQL 2024-06-02 19:03:58 +02:00
8c11166397 Refactoring 2024-06-02 19:03:00 +02:00
57b5b4ffab Foreign Keys 2024-05-21 15:46:46 +02:00
bf6d3b737a Setup Tests 2024-05-21 13:29:43 +02:00
2153bb07b9 Tests 2024-05-20 20:36:46 +02:00
d6f7fa0661 Fix multiple Teacher 2024-04-17 14:53:02 +02:00
0a4cca102b Rapla-Scheduler 2024-04-17 14:36:43 +02:00
dd052aeded Rapla-Key-Import + Depreciate calendars/list.json 2024-04-17 13:56:16 +02:00
e2eec5794e Fix "nicht bestanden" - Format 2024-04-15 10:13:10 +02:00
e700a7ad97 Fix "nicht bestanden" 2024-04-15 10:00:48 +02:00
8e0217087f Create README; Set-Up Streamline 2024-04-14 16:10:37 +02:00
1d30c07cdc Create README; Set-Up Streamline 2024-04-14 15:25:32 +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
38 changed files with 1570 additions and 748 deletions

1
.gitignore vendored
View File

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

33
README.md Normal file
View File

@ -0,0 +1,33 @@
# 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

@ -1,8 +1,12 @@
import time
import asyncio
import icalendar import icalendar
import datetime 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"]
@ -10,14 +14,14 @@ 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
:param weekstart: :param weekstart:
:param file: :param file:
:param showsat: :param showsat:
:return: :return (Event-Liste, Essens-Liste, voheriges Wochendatum, nächstes Wochendatum, Datum des Montags):
""" """
if weekstart == "today": if weekstart == "today":
start_date = datetime.date.today() start_date = datetime.date.today()
@ -37,8 +41,10 @@ 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":
@ -57,6 +63,13 @@ 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,
@ -64,18 +77,40 @@ 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, daylist(start_date, showsat), prevw, nextw, mon return eventl, await MensaDayList(start_date, showsat), prevw, nextw, mon
def daylist(weekstart: datetime, showsat: bool): async def createCustomCalendar (uid: int):
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: :return Essens-Liste:
""" """
weekday = weekstart weekday = weekstart
dayl = [] dayl = []
@ -83,15 +118,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,6 +0,0 @@
{
"TINF22B3": [
"raplaTINF22B3.ical",
"https://rapla.dhbw-karlsruhe.de/rapla?page=ical&user=vollmer&file=tinf22b3"
]
}

View File

@ -1 +1 @@
25454 18291

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,69 +17,73 @@ 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, Cookie):
""" """
s = requests.Session() async with httpx.AsyncClient() as s:
fpw = urllib.parse.quote(password, safe='', encoding=None, errors=None) formattedPassword = urllib.parse.quote(password, safe='', encoding=None, errors=None)
fmail = urllib.parse.quote(email, safe='', encoding=None, errors=None) formattedEmail = 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={formattedEmail}&pass={formattedPassword}&ARGUMENTS=clino%2Cusrname%2Cpass%2C"
'%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK') f"menuno%2Cmenu_type%2Cbrowser%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
:param token: :param token:
:param cookie: :param cookie:
:return Kurs-Bezeichner: :return Kurs-Bezeichner ODER 0 bei Fehler:
""" """
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'] try:
response = requests.request("GET", url + link[21:], headers=headers, data={}) link = html.body.find('a', attrs={'id': "Popup_details0001"})['href']
html = BeautifulSoup(response.text, 'lxml') except TypeError:
content = html.body.find('td', attrs={'class': 'level02'}).text return 0
start = content.find(" ") + 4 response = await s.get(url=f"{url}{link[21:]}", headers=headers)
end = start + (content[start:].find(" ")) html = BeautifulSoup(response.text, 'lxml')
kurs = content[start:end] content = html.body.find('td', attrs={'class': 'level02'}).text
start = content.find(" ") + 4
end = start + (content[start:].find(" "))
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 +92,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") text = i.text.replace("Wi", "Winter").replace("So", "Sommer")
t = t.replace("Se", "semester") text = text.replace("Se", "semester")
optlist += [[t, i['value']]] optlist += [[text, 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,46 +115,58 @@ 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: vorlesungen = body.find_all("tr")
cols = row.find_all("td") vorlesungenList = []
col = [[e.text.strip()] for e in cols] tasks = []
if len(col) != 0: i = 0
if len(col[4][0]) == 0: for row in vorlesungen:
col[2] = getPruefung(row.find("a")["href"]) columns = row.find_all("td")
vorlist += [col[1:4]] column = [[e.text.strip()] for e in columns]
return vorlist if len(column) != 0:
if len(column[4][0]) == 0 or len(column[2][0]) == 0:
tasks += [getPruefung(s, row.find("a")["href"])]
column[2] = i
i += 1
vorlesungenList += [column[1:4]]
notlisted = await asyncio.gather(*tasks, return_exceptions=True)
for i in vorlesungenList:
for e in range(0, len(i)):
if isinstance(i[e], int):
i[e] = notlisted[i[e]]
return vorlesungenList[:-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 list: :return Noten-Liste:
""" """
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 = [] returnList = []
for row in pruefung: for row in pruefung:
cols = row.find_all("td") columns = row.find_all("td")
col = [e.text.strip() for e in cols] column = [e.text.strip() for e in columns]
if len(col) == 6 and len(col[3]) <= 13: if len(column) == 6 and len(column[3]) <= 13:
if len(col[3]) != 0: if len(column[3]) != 0:
ret += [col[0] + ": " + col[3][:3]] returnList += [column[0] + ": " + column[3].split("\xa0")[0]]
if ret[-1][0] == ':': if returnList[-1][0] == ':':
ret[-1] = "Gesamt" + ret[-1] returnList[-1] = "Gesamt" + returnList[-1]
if len(ret) == 0: if len(returnList) == 0:
ret = ["Noch nicht gesetzt"] returnList = ["Noch nicht gesetzt"]
return ret return returnList
def checkLifetime(timecode: float): def checkLifetime(timecode: float):

View File

@ -1,13 +1,16 @@
import json import json
from init import db, Meals, scheduler
import asyncio
from init import db, Meals, scheduler, flaskApp
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,46 +38,47 @@ 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")) jsonResponse = json.loads(response.decode("utf-8"))
essen = [] essen = []
try: try:
num = len(jres["data"][0]["lines"]) number = len(jsonResponse["data"][0]["lines"])
for i in range(num): for i in range(number):
try: try:
jsmeal = jres["data"][0]["lines"][i]["meals"] jsonMeals = jsonResponse["data"][0]["lines"][i]["meals"]
cont = True hasContent = True
except IndexError: except IndexError:
essen = [] essen = []
cont = False hasContent = False
if cont: if hasContent:
for e in range(len(jsmeal)): for e in range(len(jsonMeals)):
ji = jsmeal[e] jsonEntry = jsonMeals[e]
name = ji["name"] name = jsonEntry["name"]
if pricetofloat(ji["price"]) >= 1.1: if pricetofloat(jsonEntry["price"]) >= 1.1:
vegan = ji["classifiers"].count("VG") == 1 vegan = jsonEntry["classifiers"].count("VG") == 1
schwein = ji["classifiers"].count("S") == 1 schwein = jsonEntry["classifiers"].count("S") == 1
if vegan: if vegan:
veget = True vegetarian = True
else: else:
veget = ji["classifiers"].count("VEG") == 1 vegetarian = jsonEntry["classifiers"].count("VEG") == 1
if veget: if vegetarian:
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=veget, schwein=schwein) neu = Meals(date=day, name=name, id=mid, vegan=vegan, vegetarian=vegetarian,
db.session.add(neu) schwein=schwein)
db.session.commit() db.session.add(neu)
if not essen: db.session.commit()
if not essen:
essen = [nomeal]
except KeyError:
essen = [nomeal] essen = [nomeal]
except KeyError: return essen
essen = [nomeal]
return essen
def pricetofloat(price: str): def pricetofloat(price: str):
@ -99,40 +103,49 @@ def formatDay(day: datetime):
:return str: :return str:
""" """
if day.month < 10: if day.month < 10:
mon = "0" + str(day.month) monat = "0" + str(day.month)
else: else:
mon = str(day.month) monat = 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) + "-" + mon + "-" + tag formattedDay = str(day.year) + "-" + monat + "-" + tag
return day return formattedDay
@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 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() dbMeals = Meals.query.filter_by(date=i).all()
dbnames = [] dbNames = []
for m in dbmeals: for meal in dbMeals:
dbnames += [m.name] dbNames += [meal.name]
if set(dbnames) != set(apinames) and nomeal not in apinames: if set(dbNames) != set(apiNames) and nomeal not in apiNames:
for n in dbnames: for name in dbNames:
db.session.delete(Meals.query.filter_by(date=i, name=n).first()) db.session.delete(Meals.query.filter_by(date=i, name=name).first())
db.session.commit() db.session.commit()
getMealsFromAPI(i, True) await getMealsFromAPI(i, True)
@scheduler.task('cron', id="mensaSchedule", hour='8-11', day_of_week='*', minute='*/15', week='*', second='5')
def mensaSchedule():
"""
Nutzt vormittags die Funktion refreshMeals(), um die Essen zu aktualisieren
"""
if not flaskApp.config["TESTING"]:
with flaskApp.app_context():
asyncio.run(refreshMeals())

View File

@ -1,18 +1,60 @@
import urllib.error import urllib.error
from urllib.request import urlretrieve from datetime import datetime, timedelta
from dateutil.parser import *
import asyncio
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
from init import scheduler, flaskApp, Rapla, db, HiddenVL, User
from calendar_generation import createCustomCalendar
def parseURL(url: str): async def fetchPlan(session, url):
""" """
Konvertiert URLs ins korrekte Format. \n Hilfsfunktion, liefert die Response auf einen GET-Request zur angegebenen URL
:param session:
:param url:
:return Response:
"""
return await session.get(url=url)
def writeToFile(filename, data):
"""
Schreibt die Daten in die angegebene Datei. Erstellt die Datei, falls sie noch nicht existiert.
:param filename:
:param data:
"""
with open(filename, 'w+') as file:
assert "BEGIN:VCALENDAR" in data.text
file.write(data.text)
file.close()
def writeKursToDB(kurs, url):
"""
Schreibt Kurs und URL in die Datenbank
: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 str: :return [Status: int, URL: str]:
""" """
rapla = url.find("rapla.") rapla = url.find("rapla.")
if rapla == -1: if rapla == -1:
@ -21,85 +63,197 @@ def parseURL(url: str):
url = url[rapla:] url = url[rapla:]
http = url.find(":") http = url.find(":")
if url[:http] == "http": if url[:http] == "http":
url = "https" + url[http:] url = f"https{url[http:]}"
elif http == -1: elif http == -1:
url = "https://" + url url = f"https://{url}"
p = url.find("page=") pageInURL = url.find("page=")
u = url.find("&") andInURL = url.find("&")
if (url[p + 5:u]).lower() == "ical": if (url[pageInURL + 5:andInURL]).lower() == "ical":
return url return 1, url
elif p != -1: elif pageInURL != -1:
return url[:p + 5] + "ical" + url[u:] return 1, f"{url[:pageInURL + 5]}ical{url[andInURL:]}"
elif url.find("key") != -1:
return 2, url
else: else:
return 0 return 0, 0
def getNewRapla(url: str): async def getNewRapla(url: str, testing=False):
""" """
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:
""" """
url = parseURL(url) parsed = parseRaplaURL(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
urlfile = url.find("file=") elif parsed[0] == 1:
kurs = url[urlfile + 5:].upper() url = parsed[1]
elif parsed[0] == 2:
try: return await buildICALfromKey(parsed[1], onlyUpdate=False, testing=testing)
urlretrieve(url, "calendars/rapla" + kurs + ".ical") fileInURL = url.find("file=")
except urllib.error.URLError: kurs = url[fileInURL + 5:].replace(" ", "").upper()
return -1 if url[-5:] != ".ical":
try:
file = open("calendars/list.json", "r+") async with httpx.AsyncClient() as s:
jsoncal = json.load(file) response = await fetchPlan(s, url)
jsoncal.update({kurs: ["rapla" + kurs + ".ical", url]}) if testing:
file.close() assert "Vollmer" in response.text
file = open("calendars/list.json", "w") writeToFile(f"calendars/rapla{kurs}.ical", response)
json.dump(jsoncal, file, indent=4) except urllib.error.URLError:
return "rapla" + kurs + ".ical" return -1
writeKursToDB(kurs, url)
if testing:
assert "TINF22B3" in Rapla.query.filter(Rapla.name == "TINF22B3").first().file
return f"rapla{kurs}.ical"
else:
return url
def getIcal(kurs: str): def getIcal(uid: int | None = None, kurs: str | None = None):
""" """
Liefert den Namen der Datei des mitgegebenen Kurses. Liefert den Namen der Datei des mitgegebenen Users.
:param uid:
:param kurs: :param kurs:
:return str: :return str:
""" """
file = open("calendars/list.json", "r") if uid:
jf = json.load(file) if HiddenVL.query.filter_by(uid=uid).first():
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 jf[kurs][0] return rapla.file
except KeyError: except AttributeError or KeyError:
return None return None
def getRaplas(): def getRaplas():
""" """
Liefert alle auf dem Server gespeicherten Raplas. Liefert alle in der Datenbank gespeicherten Raplas.
:return (Kursliste, Dateiliste, URL-Liste): :return (Kursliste, Dateiliste, URL-Liste):
""" """
file = open("calendars/list.json", "r") raplas = Rapla.query.all()
jsonf = json.load(file) kursList = [rapla.name for rapla in raplas]
kursl = [] fileList = [rapla.file for rapla in raplas]
filel = [] urlList = [rapla.link for rapla in raplas]
urll = [] return kursList, fileList, urlList
for i in jsonf:
kursl += [i]
filel += [jsonf[i][0]]
urll += [jsonf[i][1]]
return sorted(kursl), sorted(filel), sorted(urll)
@scheduler.task("interval", id="refreshRapla", minutes=5) async def refreshRapla():
def refreshRapla():
""" """
Aktualisiert alle 5 Minuten alle gespeicherten Raplas. Aktualisiert alle gespeicherten Raplas.
""" """
filel = getRaplas()[1] fileList = getRaplas()[1]
urll = getRaplas()[2] urlList = getRaplas()[2]
for i in range(len(filel)): jobList = []
print("Update Rapla: " + filel[i][:-5]) async with httpx.AsyncClient() as s:
urlretrieve(urll[i], "calendars/" + filel[i]) for i in range(len(fileList)):
print(f"Update Rapla: {fileList[i][:-5]}")
if urlList[i].find("file") != -1:
jobList += [fetchPlan(s, urlList[i])]
else:
jobList += [buildICALfromKey(urlList[i], onlyUpdate=True)]
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')
def raplaSchedule():
"""
Nutzt alle 3 Minuten refreshRapla() um die Stundenpläne zu aktualisieren.
"""
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

42
genSCSSstarts.py Normal file
View File

@ -0,0 +1,42 @@
"""
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()

View File

@ -1,40 +0,0 @@
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()

23
getMySQL.py Normal file
View File

@ -0,0 +1,23 @@
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"

View File

@ -1,17 +0,0 @@
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

View File

@ -1,6 +0,0 @@
#!/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));"

71
init.py
View File

@ -2,21 +2,27 @@ 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 get_mysql import get_mysql from sqlalchemy import ForeignKey
import sys
from getMySQL import getMySQL
import atexit import atexit
from flask_apscheduler import APScheduler from flask_apscheduler import APScheduler
def create(): def create(testing: bool = False):
""" """
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 = get_mysql()[1] dbpw = getMySQL()[1]
dbun = get_mysql()[0] dbun = getMySQL()[0]
app.config['SECRET_KEY'] = 'SECRET_KEY_GOES_HERE' try:
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)
@ -25,7 +31,8 @@ def create():
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
atexit.register(lambda: scheduler.shutdown()) if not testing:
atexit.register(lambda: scheduler.shutdown())
@login_manager.user_loader @login_manager.user_loader
def load_user(uid: int): def load_user(uid: int):
@ -41,6 +48,7 @@ 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))
@ -51,29 +59,66 @@ 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, primary_key=True) uid = db.Column(db.Integer, ForeignKey('user.id', ondelete='CASCADE'), 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.
"""
__tablename__ = 'semesterlist'
uid = db.Column(db.Integer, ForeignKey('user.id', ondelete='CASCADE'))
semestername = db.Column(db.String(25))
semesterid = db.Column(db.String(15))
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(100)) name = db.Column(db.String(200))
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()
app = create() flaskApp = create()
with flaskApp.app_context():
print("Creating Tables....")
db.create_all()
def_src = ["*.paulmartin.cloud", '\'self\''] def_src = ["*.paulmartin.cloud", '\'self\'']
Talisman(app, content_security_policy={"default-src": def_src, "script-src": def_src # + ["'unsafe-inline'"] Talisman(flaskApp, content_security_policy={"default-src": def_src, "script-src": def_src # + ["'unsafe-inline'"]
}) })
scheduler.init_app(app) scheduler.init_app(flaskApp)
scheduler.start() scheduler.start()
scheduler.api_enabled = True scheduler.api_enabled = True

View File

@ -1,10 +1,63 @@
from flask_login import current_user as currentUser
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: :return Cookie:
""" """
cookie = 0 cookie = 0
for c in cookies: for c in cookies:
cookie = c.value cookie = c.value
return cookie return cookie
async def getSemesterList(uid, token, cookie):
"""
Liefert die IDs der Semester für den User
:param uid:
:param token:
:param cookie:
:return Semester-ID-Liste:
"""
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):
"""
Hilfsfunktion, die den User für die UID zurückgibt.
:param uid:
:return User:
"""
return User.query.filter_by(id=uid).first()

View File

@ -1,13 +1,24 @@
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
talisman talisman
Werkzeug Werkzeug~=3.0.0
lxml lxml
bs4 bs4~=0.0.1
pytz pytz~=2023.3.post1
flask_talisman
asyncio~=3.4.3
httpx
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,343 +1,390 @@
#!/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 from flask import render_template, url_for, redirect, request, send_from_directory
from flask_login import login_user, login_required, current_user, logout_user from flask_login import (login_user as loginUser, login_required as loginRequired,
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 fetchRAPLA import fetchRAPLA
import requesthelpers from requesthelpers import *
from fetchRAPLA import * from fetchRAPLA import *
from calendar_generation import getWeek from calendar_generation import getWeek, createCustomCalendar
from init import * from init import *
@app.route("/") def initRoutes(app: Flask):
def index():
""" """
Leitet den normalen Website-Aufruf zum Login weiter. Initialisiert die App-Routen. Nötig für Tests.
:return HTML: :param app:
""" """
return redirect(url_for("login"))
@app.route("/")
def index():
"""
Leitet den normalen Website-Aufruf zum Login weiter.
:return HTML:
"""
return redirect(url_for("login"))
@app.route("/dashboard") @app.route("/dashboard")
@login_required @loginRequired
def welcome(): def welcome():
""" """
Dashboard Dashboard
:return HTML: :return HTML:
""" """
if not current_user.kurs: if not currentUser.kurs:
return redirect(url_for("getKurs", next=url_for(request.endpoint))) return redirect(url_for("getKurs", next=url_for(request.endpoint)))
sel = request.args.get("sel") selectedPhase = request.args.get("sel")
if not sel: if not selectedPhase:
sel = "theorie" selectedPhase = "theorie"
kurs = current_user.kurs kurs = currentUser.kurs
name = current_user.name name = currentUser.name
if sel == "theorie": if selectedPhase == "theorie":
t = "" theorie = ""
p = "hidden" praxis = "hidden"
else: else:
t = "hidden" theorie = "hidden"
p = "" praxis = ""
return render_template('dashboard.html', kurs=kurs, name=name, theorie=t, praxis=p) return render_template('dashboard.html', kurs=kurs, name=name, theorie=theorie, praxis=praxis)
@app.route("/theorie/noten", methods=["GET", "POST"])
@loginRequired
async def displayNoten():
"""
Zeigt die Noten aus Dualis an. Hierfür ist ein aktives Token nötig.
:return HTML:
"""
dualisUser = Dualis.query.filter_by(uid=currentUser.id).first()
if request.method == "POST":
dualisUser.semester = request.form.get("sem")
db.session.commit()
if not dualisUser.semester:
return redirect(url_for("getSemester", next=url_for(request.endpoint)))
token = dualisUser.token
chosenSemester = dualisUser.semester
cookie = request.cookies.get("cnsc")
timeout = fetchDUALIS.timeOut(dualisUser, cookie, "displayNoten")
if timeout:
return timeout
semester = await getSemesterList(currentUser.id, token, cookie)
noten = await fetchDUALIS.getResults(token, cookie, chosenSemester)
return render_template("noten.html", noten=noten, semester=semester, sel=chosenSemester, s="n",
praxis="hidden")
@app.route("/theorie/noten") @app.route("/plan", methods=["GET"])
@login_required @loginRequired
def displayNoten(): async def displayRapla():
""" """
Zeigt die Noten aus Dualis an. Hierfür ist ein aktives Token nötig. Zeigt den Stundenplan für eingeloggte User an. \n
:return HTML: TODO: Persönliche Filter, Notizen, Essensvorlieben etc. berücksichtigen.
""" :return HTML:
d = Dualis.query.filter_by(uid=current_user.id).first() """
if not d.result_list: if not currentUser.kurs:
return redirect(url_for("getSemester", next=url_for(request.endpoint))) return redirect(url_for("getKurs", next=url_for(request.endpoint)))
t = d.token week = request.args.get("week")
sem = d.result_list if week and week!="today":
c = request.cookies.get("cnsc") week = datetime.strptime(week, "%Y-%m-%d")
timeout = fetchDUALIS.timeOut(d, c, "displayNoten") else:
if timeout: week = "today"
return timeout
res = fetchDUALIS.getResults(t, c, sem)
return render_template("noten.html", noten=res, semester=fetchDUALIS.getSem(t, c), sel=sem, s="n", praxis="hidden")
@app.route("/plan", methods=["GET"])
@login_required
def displayRapla():
"""
Zeigt den Stundenplan für eingeloggte User an. \n
TODO: Persönliche Filter, Notizen, Essensvorlieben etc. berücksichtigen.
:return HTML:
"""
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 = 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>")
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 = getWeek(week, plan, samstag) events = await getWeek(week, fetchRAPLA.getIcal(uid=currentUser.id), samstag)
return render_template("plan-anon.html", events=events[0], eventdays=events[1], kurs=kurs, return render_template("plan-user.html", events=events[0], eventdays=events[1],
prev=str(events[2])[:10], next=str(events[3])[:10], mon=events[4], praxis="hidden") uid=currentUser.id, name=currentUser.name, prev=str(events[2])[:10],
else: next=str(events[3])[:10], mon=events[4], login=True, s="p", date=str(week)[:10],
return redirect(url_for("login")) praxis="hidden")
@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("/set-up") @app.route("/plan/restore")
def redKurs(): @loginRequired
""" async def restoreVL():
Setup beginnt mit Kurs. events = HiddenVL.query.filter_by(uid=currentUser.id).all()
:return HTML: return render_template("restore-events.html", s="s", events=events, praxis="hidden")
"""
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/kurs") @app.route("/set-up")
@login_required def redKurs():
def getKurs(): """
""" Setup beginnt mit Kurs.
Automatische Kurs-Auswahl. \n :return HTML:
Aktives Dualis-Token benötigt. """
:return HTML: return redirect(url_for("getKurs"))
"""
d = Dualis.query.filter_by(uid=current_user.id).first() @app.route("/set-up/kurs")
if d: @loginRequired
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")
timeout = fetchDUALIS.timeOut(d, cookie, "getKurs") semesterList = Semesterlist.query.filter_by(uid=currentUser.id).all()
if timeout: if not semesterList:
return timeout semester = await semesterDualisToDB([], token, cookie)
e = False else:
if not current_user.kurs: semester = await getSemesterList(currentUser.id, token, cookie)
kurs = fetchDUALIS.getKurs(d.token, cookie) return render_template("semester.html", semester=semester, s="s",
if kurs != 0: theorie="hidden", praxis="hidden")
if not fetchRAPLA.getIcal(kurs):
return render_template('kurs.html', detected=(kurs, e), s="s", theorie="hidden", praxis="hidden", @app.route("/set-up/semester", methods=["POST"])
file=False) @loginRequired
current_user.kurs = kurs def setSemester():
"""
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:
e = True return redirect(url_for("error", ecode=900))
else: return redirect(url_for("welcome"))
kurs = current_user.kurs
current_user.kurs = kurs @app.route("/log-in")
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()
else: except IntegrityError:
e = True return redirect(url_for("displayRapla", week=day))
kurs = "" await createCustomCalendar(currentUser.id)
return render_template('kurs.html', detected=(kurs, e), s="s", theorie="hidden", praxis="hidden", file=True) return redirect(url_for("displayRapla", week=day))
@app.route("/plan/restore/event")
@app.route("/set-up/semester") @loginRequired
@login_required async def restoreEvent():
def getSemester(): id = str(request.args.get("id"))
""" uid = currentUser.id
Manuelle Semester-Auswahl. entry = HiddenVL.query.filter_by(id=str(uid) + id).first()
:return HTML: db.session.delete(entry)
"""
t = Dualis.query.filter_by(uid=current_user.id).first().token
c = request.cookies.get("cnsc")
return render_template("semester.html", semester=fetchDUALIS.getSem(t, c), s="s", theorie="hidden", praxis="hidden")
@app.route("/set-up/semester", methods=["POST"])
@login_required
def setSemester():
"""
Speichern der Semester-Auswahl.
:return HTML:
"""
n = request.args.get("next")
if not n:
n = url_for("welcome")
d = Dualis.query.filter_by(uid=current_user.id).first()
d.result_list = request.form.get("sem")
db.session.commit()
return redirect(n)
@app.route("/set-up/rapla")
@login_required
def chooseRaplas():
"""
Manuelle Rapla-Auswahl.
:return HTML:
"""
r = getRaplas()
return render_template("rapla.html", raplas=r, s="s", theorie="hidden", praxis="hidden")
@app.route("/set-up/rapla", methods=["POST"])
@login_required
def getRapla():
"""
Verarbeitet die Eingabe von chooseRaplas().
:return HTML:
"""
file = str(request.form.get("file"))
url = str(request.form.get("url"))
if file == url == "None":
return redirect(url_for("chooseRaplas"))
if file != "None":
User.query.filter_by(id=current_user.id).first().kurs = file[5:-5]
db.session.commit() db.session.commit()
elif url != "None": await createCustomCalendar(currentUser.id)
file = getNewRapla(url) return redirect(url_for("restoreVL"))
if type(file) is not int:
User.query.filter_by(id=current_user.id).first().kurs = file[5:-5] @app.route("/log-in", methods=["POST"])
db.session.commit() 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: else:
return redirect(url_for("error", ecode=900)) success = make_response(redirect(url_for("getKurs")))
return redirect(url_for("welcome"))
user = User.query.filter_by(email=email).first()
tokenAndCookie = await fetchDUALIS.checkUser(email, password)
if tokenAndCookie[0] == -2:
return redirect(url_for("login", code=-2))
if user:
dualisUser = Dualis.query.filter_by(uid=user.id).first()
dualisUser.token = tokenAndCookie[0]
newCookie = tokenAndCookie[1]
dualisUser.token_created = time.time()
db.session.commit()
loginUser(user)
if user.kurs:
if not dualisUser.semester:
success = make_response(redirect(url_for("getSemester")))
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]
@app.route("/log-in") newDualis = Dualis(uid=hashedID, token=tokenAndCookie[0], token_created=int(time.time()))
def login(): db.session.add(newDualis)
""" db.session.commit()
Login-Maske. loginUser(new_user)
:return HTML: success.set_cookie("cnsc", value=cookie, httponly=True, secure=True)
""" return success
return render_template("login.html", theorie="hidden", praxis="hidden", s="s")
@app.route("/log-out")
@app.route("/log-in", methods=["POST"]) @loginRequired
def login_post(): async def logout():
""" """
Verarbeitet die Eingabe von login(). \n Loggt den User aus.
Falls der User schon angelegt ist, wird das Passwort verglichen. \n :return Empty Token:
Falls nicht, wird ein neuer angelegt. """
:return HTML: cookie = request.cookies.get("cnsc")
""" dualisUser = Dualis.query.filter_by(uid=currentUser.id).first()
email = request.form.get("email") await fetchDUALIS.logOut(dualisUser.token, cookie)
password = request.form.get("password") dualisUser.token = None
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 = 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 = requesthelpers.getCookie(t[1].cookies)
dualis.token_created = time.time()
db.session.commit() db.session.commit()
login_user(user) logoutUser()
if user.kurs: redirection = make_response(redirect(url_for("login", code=1, next=url_for("welcome"))))
if not dualis.result_list: redirection.set_cookie("cnsc", value="Logged out! Your temporary token "
success = make_response(redirect(url_for("getSemester"))) "on our server and the cookie on your device have been deleted.",
elif not n: httponly=True,
success = make_response(redirect(url_for("welcome"))) secure=True)
success.set_cookie("cnsc", value=newcookie, httponly=True, secure=True) return redirection
else: @app.route("/error")
hashid = int(hashlib.sha1(email.encode("utf-8")).hexdigest(), 16) % (10 ** 8) def error():
pname = email.find(".") + 1 """
ename = min(email[pname:].find("."), email[pname:].find("@")) Error Page für custom-Errors. \n
name = email[pname:pname + ename].capitalize() TODO: Funktion depreciaten. Ersetzen durch Errors auf den entsprechenden Seiten.
new_user = User(email=email, name=name, id=hashid) :return HTML:
db.session.add(new_user) """
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:
message = str(errorCode)
return render_template('display-message.html', message=message)
cookie = requesthelpers.getCookie(t[1].cookies) @app.route("/error")
@app.errorhandler(HTTPException)
new_dualis = Dualis(uid=hashid, token=t[0], token_created=int(time.time())) def handle(e):
db.session.add(new_dualis) """"
db.session.commit() HTTP-Exception-Handler
login_user(new_user) :param e:
success.set_cookie("cnsc", value=cookie, httponly=True, secure=True) :return HTML:
return success """
return render_template('display-message.html', message=e)
@app.route("/log-out")
@login_required
def logout():
"""
Loggt den User aus.
:return Empty Token:
"""
cookie = request.cookies.get("cnsc")
dualis = Dualis.query.filter_by(uid=current_user.id).first()
fetchDUALIS.logOut(dualis.token, cookie)
dualis.token = None
db.session.commit()
logout_user()
red = make_response(redirect(url_for("login", code=1, 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__":
app.run(host='0.0.0.0', port=2024, debug=True) initRoutes(flaskApp)
flaskApp.run(host='0.0.0.0', port=2024, debug=True)
else:
initRoutes(flaskApp)

View File

@ -68,6 +68,11 @@ 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;
@ -100,7 +105,8 @@ option {
.timeline { .timeline {
display: grid; display: grid;
grid-template-rows: repeat(12, 58px); grid-template-rows: repeat(12, 60px);
padding-top: 50px;
} }
.days { .days {
@ -126,7 +132,8 @@ option {
} }
.room, .room,
.time { .time,
.teacher {
display: block; display: block;
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
@ -194,7 +201,9 @@ 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;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"} {"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"}

View File

@ -68,6 +68,11 @@ 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;
@ -86,7 +91,7 @@ button:hover{
} }
.plusbutton { .plusbutton {
font-size: 1.5em font-size: 1.5em;
} }
form { form {
@ -100,7 +105,8 @@ option {
.timeline { .timeline {
display: grid; display: grid;
grid-template-rows: repeat(12, 58px); grid-template-rows: repeat(12, 60px);
padding-top: 50px;
} }
.days { .days {
@ -128,7 +134,8 @@ option {
} }
.room, .room,
.time { .time,
.teacher {
display: block; display: block;
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
@ -201,7 +208,9 @@ 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 {
@ -276,7 +285,7 @@ a.footer:hover {
// Place on Timeline // Place on Timeline
// Generated by genstarts.py // Generated by genSCSSstarts.py
.start-0100 { .start-0100 {
grid-row-start: 1 grid-row-start: 1

1
static/motd.js Normal file
View File

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

View File

@ -22,6 +22,8 @@ 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 {
@ -89,6 +91,11 @@ 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;;;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"} {"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"}

View File

@ -22,6 +22,8 @@ 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 {
@ -89,6 +91,11 @@ nav li .bottom {
padding: 20px; padding: 20px;
} }
a {
color: white;
font-size: 110%;
}
.container { .container {
display: flex; display: flex;

View File

@ -4,7 +4,7 @@
<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 async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script> <script src={{ url_for("static", filename="motd.js") }}></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}
</head> </head>

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 action={{ url_for("getSemester") }}> <form id="autoForm" 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 action={{ url_for("chooseRaplas", next=request.args.get("next")) }}> <form id="manualForm" 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

@ -27,7 +27,7 @@
{% endfor %} {% endfor %}
<br> <br>
<br> <br>
<form id= "dropdown" method="post" action={{ url_for ("setSemester", next=url_for("displayNoten")) }}> <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) %}

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 async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script> <script src={{ url_for("static", filename="motd.js") }}></script>
</head> </head>
<body> <body>
<nav> <nav>
@ -63,12 +63,26 @@
<div class="events"> <div class="events">
{% for i in events %} {% for i in events %}
{% if i["weekday"] == e %} {% if i["weekday"] == e %}
<div class="event start-{{ i["start"][:2]+i["start"][3:] }} end-{{ i["end"][:2]+i["end"][3:]}}"> {% with event=i %}
{% block event %} {% endblock %} <div class="event start-{{ event["start"][:2]+event["start"][3:] }} end-{{ event["end"][:2]+event["end"][3:]}}">
<p class="title">{{ i["name"] }}</p> {% if login %}
<p class="room">{{ i["room"] }}</p> <div class="userbuttons">
<p class="time">{{ i["start"] }} - {{ i["end"] }} ({{ i["dur"] }}h)</p> <form method="post" action={{ url_for ("removeEvent", id=event["id"], day=date, name=event["name"]) }}>
</div> <button>👁</button>
</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,11 +5,8 @@
{% block startcontent %} {% block startcontent %}
<h2>{{ name }}s Vorlesungsplan</h2> <h2>{{ name }}s Vorlesungsplan</h2>
{% endblock %} {% endblock %}
{% block event %}
<div class="userbuttons">
<button>👁</button>
<button class="plusbutton">+</button>
</div>
{% endblock %}
{% block endcontent %} {% block endcontent %}
<a href={{ url_for("deliverCalendar", uid=uid) }}>Kalender-Abo</a>
<br>
<a href={{ url_for("restoreVL") }}>Ausgeblendete Vorlesungen wieder einblenden</a>
{% 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 method="post" action={{ url_for ("getRapla") }}> <form id="dbForm" 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 method="post" action={{ url_for ("getRapla") }}> <form id="manualForm" 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

@ -0,0 +1,12 @@
{% 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 %}

View File

@ -1,16 +0,0 @@
<!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,97 +0,0 @@
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

View File

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

182
tests_examples/test_app.py Normal file
View File

@ -0,0 +1,182 @@
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

287
tests_examples/vergleich.py Normal file
View File

@ -0,0 +1,287 @@
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 password, 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(password, 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:
# noinspection DuplicatedCode
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)
# noinspection DuplicatedCode
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]]
# noinspection DuplicatedCode
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)

View File

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