26 Commits
turbo ... main

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
37 changed files with 1141 additions and 701 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

@ -6,6 +6,7 @@ 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"]
@ -20,7 +21,7 @@ async def getWeek(weekstart: datetime, file: str, showsat: bool):
: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()
@ -40,8 +41,10 @@ async def getWeek(weekstart: datetime, file: str, showsat: bool):
mon += " " + str(end_date.year) mon += " " + str(end_date.year)
with open("calendars/" + file) as f: with open("calendars/" + file) as f:
calendar = icalendar.Calendar.from_ical(f.read()) calendar = icalendar.Calendar.from_ical(f.read())
events = recurring_ical_events.of(calendar).between(start_date, end_date) events = recurring_ical_events.of(calendar).between(start_date, end_date)
eventl = [] eventl = []
for event in events: for event in events:
estart = event["DTSTART"].dt estart = event["DTSTART"].dt
if str(estart.tzinfo) != "Europe/Berlin": if str(estart.tzinfo) != "Europe/Berlin":
@ -60,6 +63,13 @@ async def getWeek(weekstart: datetime, file: str, showsat: bool):
forml[i] = "0" + forml[i] forml[i] = "0" + forml[i]
formstart = forml[0] formstart = forml[0]
formend = forml[1] formend = forml[1]
try:
if type(event["ATTENDEE"]) is not list:
teacher = [event["ATTENDEE"].params["CN"]]
else:
teacher = [i.params["CN"] for i in event["ATTENDEE"]]
except KeyError:
teacher = ""
eventdict = { eventdict = {
"start": formstart, "start": formstart,
"end": formend, "end": formend,
@ -67,18 +77,40 @@ async def getWeek(weekstart: datetime, file: str, showsat: bool):
"name": event["SUMMARY"], "name": event["SUMMARY"],
"room": event["LOCATION"], "room": event["LOCATION"],
"weekday": estart.weekday(), "weekday": estart.weekday(),
"day": estart.day "day": estart.day,
"teacher": teacher,
"id": event["UID"]
} }
eventl += [eventdict] eventl += [eventdict]
return eventl, await daylist(start_date, showsat), prevw, nextw, mon return eventl, await MensaDayList(start_date, showsat), prevw, nextw, mon
async 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 = []

View File

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

View File

@ -1 +1 @@
25454 18291

View File

@ -22,13 +22,13 @@ 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):
""" """
async with httpx.AsyncClient() as s: 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)
content = (f'usrname={fmail}&pass={fpw}&ARGUMENTS=clino%2Cusrname%2Cpass%2Cmenuno%2Cmenu_type%2Cbrowser' content = (f"usrname={formattedEmail}&pass={formattedPassword}&ARGUMENTS=clino%2Cusrname%2Cpass%2C"
f'%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK') f"menuno%2Cmenu_type%2Cbrowser%2Cplatform&APPNAME=CampusNet&PRGNAME=LOGINCHECK")
response = await s.post(url=url, headers=headers, content=content) response = await s.post(url=url, headers=headers, content=content)
header = response.headers header = response.headers
try: try:
@ -48,16 +48,19 @@ async def getKurs(token: int, cookie: str):
TODO: Umstellen auf Bezeichner INKL. Standort TODO: Umstellen auf Bezeichner INKL. Standort
:param token: :param token:
:param cookie: :param cookie:
:return Kurs-Bezeichner: :return Kurs-Bezeichner ODER 0 bei Fehler:
""" """
try: try:
headers["Cookie"] = "cnsc=" + cookie headers["Cookie"] = "cnsc=" + cookie
token = str(token) token = str(token)
async with httpx.AsyncClient as s: async with httpx.AsyncClient() as s:
response = await s.get(url=f"{url}?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N{token},-N000307,", response = await s.get(url=f"{url}?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N{token},-N000307,",
headers=headers) headers=headers)
html = BeautifulSoup(response.text, 'lxml') html = BeautifulSoup(response.text, 'lxml')
try:
link = html.body.find('a', attrs={'id': "Popup_details0001"})['href'] link = html.body.find('a', attrs={'id': "Popup_details0001"})['href']
except TypeError:
return 0
response = await s.get(url=f"{url}{link[21:]}", headers=headers) response = await s.get(url=f"{url}{link[21:]}", headers=headers)
html = BeautifulSoup(response.text, 'lxml') html = BeautifulSoup(response.text, 'lxml')
content = html.body.find('td', attrs={'class': 'level02'}).text content = html.body.find('td', attrs={'class': 'level02'}).text
@ -97,9 +100,9 @@ async def getSem(token: int, cookie: str):
select = select.find_all(value=True) select = select.find_all(value=True)
optlist = [] optlist = []
for i in select: for i in select:
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
@ -119,25 +122,25 @@ async def getResults(token, cookie: str, resl: str):
html = BeautifulSoup(response.content.decode("utf-8"), 'lxml') html = BeautifulSoup(response.content.decode("utf-8"), 'lxml')
table = html.find('table', attrs={"class": "nb list"}) table = html.find('table', attrs={"class": "nb list"})
body = table.find("tbody") body = table.find("tbody")
vorl = body.find_all("tr") vorlesungen = body.find_all("tr")
vorlist = [] vorlesungenList = []
tasks = [] tasks = []
i = 0 i = 0
for row in vorl: for row in vorlesungen:
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) != 0: if len(column) != 0:
if len(col[4][0]) == 0: if len(column[4][0]) == 0 or len(column[2][0]) == 0:
tasks += [getPruefung(s, row.find("a")["href"])] tasks += [getPruefung(s, row.find("a")["href"])]
col[2] = i column[2] = i
i += 1 i += 1
vorlist += [col[1:4]] vorlesungenList += [column[1:4]]
notlisted = await asyncio.gather(*tasks, return_exceptions=True) notlisted = await asyncio.gather(*tasks, return_exceptions=True)
for i in vorlist: for i in vorlesungenList:
for e in range(0, len(i)): for e in range(0, len(i)):
if isinstance(i[e], int): if isinstance(i[e], int):
i[e] = notlisted[i[e]] i[e] = notlisted[i[e]]
return vorlist[:-1] return vorlesungenList[:-1]
async def getPruefung(s, url): async def getPruefung(s, url):
@ -146,24 +149,24 @@ async def getPruefung(s, url):
TODO: Namen der spezifischen Prüfungen auch zurückgeben, um Zusammensetzung zu spezifizieren. TODO: Namen der spezifischen Prüfungen auch zurückgeben, um Zusammensetzung zu spezifizieren.
:param s: :param s:
:param url: :param url:
:return list: :return Noten-Liste:
""" """
response = await s.get("https://dualis.dhbw.de" + url, headers=headers) response = await s.get("https://dualis.dhbw.de" + url, headers=headers)
html = BeautifulSoup(response.content.decode("utf-8"), 'lxml') html = BeautifulSoup(response.content.decode("utf-8"), 'lxml')
table = html.find('table') table = html.find('table')
pruefung = table.find_all("tr") pruefung = table.find_all("tr")
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

@ -2,7 +2,7 @@ import json
import asyncio import asyncio
from init import db, Meals, scheduler, app from init import db, Meals, scheduler, flaskApp
import datetime import datetime
import time import time
import httpx import httpx
@ -41,36 +41,37 @@ async def getMealsFromAPI(day: str, dbentry: bool = False):
async with httpx.AsyncClient() as s: async with httpx.AsyncClient() as s:
response = await s.get(url=f"https://dh-api.paulmartin.cloud/plans/{day}?canteens=erzberger") response = await s.get(url=f"https://dh-api.paulmartin.cloud/plans/{day}?canteens=erzberger")
response = response.content response = response.content
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,
schwein=schwein)
db.session.add(neu) db.session.add(neu)
db.session.commit() db.session.commit()
if not essen: if not essen:
@ -102,20 +103,20 @@ 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
async def refreshMeals(): async 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")
@ -128,20 +129,23 @@ async def refreshMeals():
for i in range(len(dates)): for i in range(len(dates)):
dates[i] = formatDay(dates[i]) dates[i] = formatDay(dates[i])
for i in dates: for i in dates:
apinames = await getMealsFromAPI(i) apiNames = await getMealsFromAPI(i)
apinames = await getMealsFromAPI(i) dbMeals = Meals.query.filter_by(date=i).all()
dbmeals = Meals.query.filter_by(date=i).all() dbNames = []
dbnames = [] for meal in dbMeals:
for m in dbmeals: dbNames += [meal.name]
dbnames += [m.name] if set(dbNames) != set(apiNames) and nomeal not in apiNames:
if set(dbnames) != set(apinames) and nomeal not in apinames: for name in dbNames:
for n in dbnames: db.session.delete(Meals.query.filter_by(date=i, name=name).first())
db.session.delete(Meals.query.filter_by(date=i, name=n).first())
db.session.commit() db.session.commit()
await getMealsFromAPI(i, True) await getMealsFromAPI(i, True)
@scheduler.task('cron', id="mensaschedule", hour='8-11', day_of_week='*', minute='*/15', week='*', second='5') @scheduler.task('cron', id="mensaSchedule", hour='8-11', day_of_week='*', minute='*/15', week='*', second='5')
def mensaschedule(): def mensaSchedule():
with app.app_context(): """
Nutzt vormittags die Funktion refreshMeals(), um die Essen zu aktualisieren
"""
if not flaskApp.config["TESTING"]:
with flaskApp.app_context():
asyncio.run(refreshMeals()) asyncio.run(refreshMeals())

View File

@ -1,33 +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 asyncio
import httpx import httpx
import icalendar import icalendar
from dateutil.relativedelta import relativedelta
from icalendar import Calendar, Event
import json import json
import recurring_ical_events
from init import scheduler, app
from init import scheduler, flaskApp, Rapla, db, HiddenVL, User
from calendar_generation import createCustomCalendar
async def fetchPlan(session, url): async def fetchPlan(session, url):
"""
Hilfsfunktion, liefert die Response auf einen GET-Request zur angegebenen URL
:param session:
:param url:
:return Response:
"""
return await session.get(url=url) return await session.get(url=url)
def writeToFile(filename, data): def writeToFile(filename, data):
with open(filename, 'w+') as f:
f.write(data.text)
f.close()
def parseURL(url: str):
""" """
Konvertiert URLs ins korrekte Format. \n 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:
@ -36,97 +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
async 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:
return 0
urlfile = url.find("file=")
kurs = url[urlfile + 5:].upper()
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
elif parsed[0] == 1:
url = parsed[1]
elif parsed[0] == 2:
return await buildICALfromKey(parsed[1], onlyUpdate=False, testing=testing)
fileInURL = url.find("file=")
kurs = url[fileInURL + 5:].replace(" ", "").upper()
if url[-5:] != ".ical":
try: try:
urlretrieve(url, "calendars/rapla" + kurs + ".ical")
async with httpx.AsyncClient() as s: async with httpx.AsyncClient() as s:
response = await fetchPlan(s, url) response = await fetchPlan(s, url)
if testing:
assert "Vollmer" in response.text
writeToFile(f"calendars/rapla{kurs}.ical", response) writeToFile(f"calendars/rapla{kurs}.ical", response)
except urllib.error.URLError: except urllib.error.URLError:
return -1 return -1
file = open("calendars/list.json", "r+") writeKursToDB(kurs, url)
jsoncal = json.load(file) if testing:
jsoncal.update({kurs: [f"rapla{kurs}.ical", url]}) assert "TINF22B3" in Rapla.query.filter(Rapla.name == "TINF22B3").first().file
file.close()
file = open("calendars/list.json", "w")
json.dump(jsoncal, file, indent=4)
return f"rapla{kurs}.ical" 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)
async def refreshRapla(): async 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]
jobl = [] jobList = []
async with httpx.AsyncClient() as s: async with httpx.AsyncClient() as s:
for i in range(len(filel)): for i in range(len(fileList)):
print(f"Update Rapla: {filel[i][:-5]}") print(f"Update Rapla: {fileList[i][:-5]}")
jobl += [fetchPlan(s, urll[i])] if urlList[i].find("file") != -1:
callist = await asyncio.gather(*jobl, return_exceptions=True) jobList += [fetchPlan(s, urlList[i])]
for cal in range(len(callist)): else:
writeToFile(f"calendars/{filel[cal]}", callist[cal]) 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='*/15', week='*') @scheduler.task('cron', id="raplaSchedule", hour='*', day_of_week='*', minute='*/3', week='*', second='40')
def raplaschedule(): def raplaSchedule():
with app.app_context(): """
Nutzt alle 3 Minuten refreshRapla() um die Stundenpläne zu aktualisieren.
"""
if not flaskApp.config["TESTING"]:
with flaskApp.app_context():
asyncio.run(refreshRapla()) 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));"

57
init.py
View File

@ -1,24 +1,28 @@
import apscheduler
from flask import Flask from flask import Flask
from flask_login import LoginManager, UserMixin from flask_login import LoginManager, UserMixin
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_talisman import Talisman from flask_talisman import Talisman
from sqlalchemy import ForeignKey
import sys
from get_mysql import get_mysql 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)
@ -27,6 +31,7 @@ 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
if not testing:
atexit.register(lambda: scheduler.shutdown()) atexit.register(lambda: scheduler.shutdown())
@login_manager.user_loader @login_manager.user_loader
@ -43,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))
@ -53,8 +59,9 @@ 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)
semester = db.Column(db.String(15)) semester = db.Column(db.String(15))
@ -63,29 +70,55 @@ class Semesterlist(db.Model):
""" """
Datenbank-Modell für Semester-Liste. Datenbank-Modell für Semester-Liste.
""" """
uid = db.Column(db.Integer) __tablename__ = 'semesterlist'
uid = db.Column(db.Integer, ForeignKey('user.id', ondelete='CASCADE'))
semestername = db.Column(db.String(25)) semestername = db.Column(db.String(25))
semesterid = db.Column(db.String(15)) semesterid = db.Column(db.String(15))
itemid = db.Column(db.Integer, primary_key=True) itemid = db.Column(db.Integer, primary_key=True)
class Rapla(db.Model):
"""
Datenbank-Modell für Rapla.
"""
__tablename__ = 'rapla'
name = db.Column(db.String(15), primary_key=True)
file = db.Column(db.String(20), unique=True)
link = db.Column(db.String(255), unique=True)
class HiddenVL(db.Model):
"""
Datenbank-Modell für ausgeblendete Vorlesungen.
"""
__tablename__ = 'hiddenVL'
uid = db.Column(db.Integer, ForeignKey('user.id', ondelete='CASCADE'))
eventid = db.Column(db.String(255))
id = db.Column(db.String(255), primary_key=True)
name = db.Column(db.String(255))
class Meals(db.Model): class Meals(db.Model):
""" """
Datenbank-Modell für Meals. Datenbank-Modell für Meals.
""" """
__tablename__ = 'meals'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.Date) date = db.Column(db.Date)
name = db.Column(db.String(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,11 +1,13 @@
from init import Semesterlist, User 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:
@ -13,14 +15,49 @@ def getCookie(cookies):
return cookie return cookie
def getSemesterList(uid): async def getSemesterList(uid, token, cookie):
semesterlist = Semesterlist.query.filter_by(uid=uid).all() """
semester = [] Liefert die IDs der Semester für den User
for s in semesterlist: :param uid:
semester += [[s.semestername, s.semesterid]] :param token:
semester.sort(key=lambda x: x[-1], reverse=True) :param cookie:
return semester :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): def loadUser(uid):
"""
Hilfsfunktion, die den User für die UID zurückgibt.
:param uid:
:return User:
"""
return User.query.filter_by(id=uid).first() return User.query.filter_by(id=uid).first()

View File

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

View File

@ -1,23 +1,28 @@
#!/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 fetchMENSA
import fetchRAPLA import fetchRAPLA
from requesthelpers import * 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 *
def initRoutes(app: Flask):
"""
Initialisiert die App-Routen. Nötig für Tests.
:param app:
"""
@app.route("/") @app.route("/")
def index(): def index():
""" """
@ -26,80 +31,78 @@ def index():
""" """
return redirect(url_for("login")) 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"]) @app.route("/theorie/noten", methods=["GET", "POST"])
@login_required @loginRequired
async def displayNoten(): async def displayNoten():
""" """
Zeigt die Noten aus Dualis an. Hierfür ist ein aktives Token nötig. Zeigt die Noten aus Dualis an. Hierfür ist ein aktives Token nötig.
:return HTML: :return HTML:
""" """
d = Dualis.query.filter_by(uid=current_user.id).first() dualisUser = Dualis.query.filter_by(uid=currentUser.id).first()
if request.method == "POST": if request.method == "POST":
d.semester = request.form.get("sem") dualisUser.semester = request.form.get("sem")
db.session.commit() db.session.commit()
if not d.semester: if not dualisUser.semester:
return redirect(url_for("getSemester", next=url_for(request.endpoint))) return redirect(url_for("getSemester", next=url_for(request.endpoint)))
t = d.token token = dualisUser.token
chosensemester = d.semester chosenSemester = dualisUser.semester
c = request.cookies.get("cnsc") cookie = request.cookies.get("cnsc")
timeout = fetchDUALIS.timeOut(d, c, "displayNoten") timeout = fetchDUALIS.timeOut(dualisUser, cookie, "displayNoten")
if timeout: if timeout:
return timeout return timeout
semester = getSemesterList(current_user.id) semester = await getSemesterList(currentUser.id, token, cookie)
noten = await fetchDUALIS.getResults(t, c, chosensemester) noten = await fetchDUALIS.getResults(token, cookie, chosenSemester)
return render_template("noten.html", noten=noten, semester=semester, sel=chosensemester, s="n", praxis="hidden") return render_template("noten.html", noten=noten, semester=semester, sel=chosenSemester, s="n",
praxis="hidden")
@app.route("/plan", methods=["GET"]) @app.route("/plan", methods=["GET"])
@login_required @loginRequired
async def displayRapla(): async def displayRapla():
""" """
Zeigt den Stundenplan für eingeloggte User an. \n Zeigt den Stundenplan für eingeloggte User an. \n
TODO: Persönliche Filter, Notizen, Essensvorlieben etc. berücksichtigen. TODO: Persönliche Filter, Notizen, Essensvorlieben etc. berücksichtigen.
:return HTML: :return HTML:
""" """
if not current_user.kurs: if not currentUser.kurs:
return redirect(url_for("getKurs", next=url_for(request.endpoint))) return redirect(url_for("getKurs", next=url_for(request.endpoint)))
week = request.args.get("week") week = request.args.get("week")
if week: if week and week!="today":
week = datetime.datetime.strptime(week, "%Y-%m-%d") week = datetime.strptime(week, "%Y-%m-%d")
else: else:
week = "today" week = "today"
samstag = request.args.get("samstag") samstag = request.args.get("samstag")
if not samstag: if not samstag:
samstag = False samstag = False
events = await getWeek(week, fetchRAPLA.getIcal(current_user.kurs), samstag) events = await getWeek(week, fetchRAPLA.getIcal(uid=currentUser.id), samstag)
return render_template("plan-user.html", events=events[0], eventdays=events[1], return render_template("plan-user.html", events=events[0], eventdays=events[1],
name=current_user.name, prev=str(events[2])[:10], next=str(events[3])[:10], mon=events[4], uid=currentUser.id, name=currentUser.name, prev=str(events[2])[:10],
s="p", praxis="hidden") next=str(events[3])[:10], mon=events[4], login=True, s="p", date=str(week)[:10],
praxis="hidden")
@app.route("/plan/<string:kurs>") @app.route("/plan/<string:kurs>")
async def displayPlan(kurs): async def displayPlan(kurs: str):
""" """
Zeigt den Stundenplan ohne Login an. \n Zeigt den Stundenplan ohne Login an. \n
Präferenzen werden nicht berücksichtigt. Präferenzen werden nicht berücksichtigt.
@ -108,26 +111,37 @@ async def displayPlan(kurs):
""" """
week = request.args.get("week") week = request.args.get("week")
if week: if week:
week = datetime.datetime.strptime(week, "%Y-%m-%d") week = datetime.strptime(week, "%Y-%m-%d")
else: else:
week = "today" week = "today"
try: try:
if current_user.kurs == kurs.upper(): if currentUser.kurs == kurs.upper():
return redirect(url_for("displayRapla")) return redirect(url_for("displayRapla"))
except AttributeError: except AttributeError:
pass pass
kurs = kurs.upper() kurs = kurs.upper()
plan = fetchRAPLA.getIcal(kurs) plan = fetchRAPLA.getIcal(kurs=kurs)
if plan: if plan:
samstag = request.args.get("samstag") samstag = request.args.get("samstag")
if not samstag: if not samstag:
samstag = False samstag = False
events = await getWeek(week, plan, samstag) events = await getWeek(week, plan, samstag)
return render_template("plan-anon.html", events=events[0], eventdays=events[1], kurs=kurs, return render_template("plan-anon.html", events=events[0], eventdays=events[1], kurs=kurs,
prev=str(events[2])[:10], next=str(events[3])[:10], mon=events[4], praxis="hidden") login=False, prev=str(events[2])[:10], next=str(events[3])[:10],
mon=events[4], praxis="hidden")
else: else:
return redirect(url_for("login")) return redirect(url_for("login"))
@app.route("/plan/restore")
@loginRequired
async def restoreVL():
events = HiddenVL.query.filter_by(uid=currentUser.id).all()
return render_template("restore-events.html", s="s", events=events, praxis="hidden")
@app.route("/plan/calendar/<int:uid>", methods=["GET"])
async def deliverCalendar(uid: int):
calstring = fetchRAPLA.getIcal(uid=uid)
return send_from_directory("calendars", calstring, as_attachment=False)
@app.route("/set-up") @app.route("/set-up")
def redKurs(): def redKurs():
@ -137,92 +151,85 @@ def redKurs():
""" """
return redirect(url_for("getKurs")) return redirect(url_for("getKurs"))
@app.route("/set-up/kurs") @app.route("/set-up/kurs")
@login_required @loginRequired
async def getKurs(): async def getKurs():
""" """
Automatische Kurs-Auswahl. \n Automatische Kurs-Auswahl. \n
Aktives Dualis-Token benötigt. Aktives Dualis-Token benötigt.
:return HTML: :return HTML:
""" """
d = Dualis.query.filter_by(uid=current_user.id).first() dualisUser = Dualis.query.filter_by(uid=currentUser.id).first()
if d: if dualisUser:
cookie = request.cookies.get("cnsc") cookie = request.cookies.get("cnsc")
timeout = fetchDUALIS.timeOut(d, cookie, "getKurs") timeout = fetchDUALIS.timeOut(dualisUser, cookie, "getKurs")
if timeout: if timeout:
return timeout return timeout
e = False dualisError = False
if not current_user.kurs: if not currentUser.kurs:
kurs = await fetchDUALIS.getKurs(d.token, cookie) kurs = await fetchDUALIS.getKurs(dualisUser.token, cookie)
if kurs != 0: if kurs != 0:
if not fetchRAPLA.getIcal(kurs): if not fetchRAPLA.getIcal(kurs=kurs):
return render_template('kurs.html', detected=(kurs, e), s="s", theorie="hidden", praxis="hidden", return render_template('kurs.html', detected=(kurs, dualisError), s="s",
file=False) theorie="hidden", praxis="hidden", file=False)
current_user.kurs = kurs currentUser.kurs = kurs
db.session.commit() db.session.commit()
else: else:
e = True dualisError = True
else: else:
kurs = current_user.kurs kurs = currentUser.kurs
current_user.kurs = kurs currentUser.kurs = kurs
db.session.commit() db.session.commit()
else: else:
e = True dualisError = True
kurs = "" kurs = ""
return render_template('kurs.html', detected=(kurs, e), s="s", theorie="hidden", praxis="hidden", file=True) return render_template('kurs.html', detected=(kurs, dualisError), s="s", theorie="hidden",
praxis="hidden", file=True)
@app.route("/set-up/semester") @app.route("/set-up/semester")
@login_required @loginRequired
async def getSemester(): async def getSemester():
""" """
Manuelle Semester-Auswahl. Manuelle Semester-Auswahl.
:return HTML: :return HTML:
""" """
t = Dualis.query.filter_by(uid=current_user.id).first().token token = Dualis.query.filter_by(uid=currentUser.id).first().token
c = request.cookies.get("cnsc") cookie = request.cookies.get("cnsc")
semesterlist = Semesterlist.query.filter_by(uid=current_user.id).all() semesterList = Semesterlist.query.filter_by(uid=currentUser.id).all()
if not semesterlist: if not semesterList:
semester = await fetchDUALIS.getSem(t, c) semester = await semesterDualisToDB([], token, cookie)
for i in semester:
semitem = Semesterlist(semestername=i[0], semesterid=i[1], uid=current_user.id, itemid=current_user.id*int(i[1][-7:])//1000000)
db.session.add(semitem)
db.session.commit()
else: else:
semester = getSemesterList(current_user.id) semester = await getSemesterList(currentUser.id, token, cookie)
return render_template("semester.html", semester=semester, s="s", theorie="hidden", praxis="hidden") return render_template("semester.html", semester=semester, s="s",
theorie="hidden", praxis="hidden")
@app.route("/set-up/semester", methods=["POST"]) @app.route("/set-up/semester", methods=["POST"])
@login_required @loginRequired
def setSemester(): def setSemester():
""" """
Speichern der Semester-Auswahl. Speichern der Semester-Auswahl.
:return HTML: :return HTML:
""" """
n = request.args.get("next") nextArg = request.args.get("next")
if not n: if not nextArg:
n = url_for("welcome") nextArg = url_for("welcome")
d = Dualis.query.filter_by(uid=current_user.id).first() dualisUser = Dualis.query.filter_by(uid=currentUser.id).first()
d.semester = request.form.get("sem") dualisUser.semester = request.form.get("sem")
db.session.commit() db.session.commit()
return redirect(n) return redirect(nextArg)
@app.route("/set-up/rapla") @app.route("/set-up/rapla")
@login_required @loginRequired
def chooseRaplas(): def chooseRaplas():
""" """
Manuelle Rapla-Auswahl. Manuelle Rapla-Auswahl.
:return HTML: :return HTML:
""" """
r = getRaplas() raplas = getRaplas()
return render_template("rapla.html", raplas=r, s="s", theorie="hidden", praxis="hidden") return render_template("rapla.html", raplas=raplas, s="s", theorie="hidden", praxis="hidden")
@app.route("/set-up/rapla", methods=["POST"]) @app.route("/set-up/rapla", methods=["POST"])
@login_required @loginRequired
async def getRapla(): async def getRapla():
""" """
Verarbeitet die Eingabe von chooseRaplas(). Verarbeitet die Eingabe von chooseRaplas().
@ -233,18 +240,17 @@ async def getRapla():
if file == url == "None": if file == url == "None":
return redirect(url_for("chooseRaplas")) return redirect(url_for("chooseRaplas"))
if file != "None": if file != "None":
loadUser(current_user.id).kurs = file[5:-5] loadUser(currentUser.id).kurs = file[5:-5]
db.session.commit() db.session.commit()
elif url != "None": elif url != "None":
file = await getNewRapla(url) file = await getNewRapla(url)
if type(file) is not int: if type(file) is not int:
User.query.filter_by(id=current_user.id).first().kurs = file[5:-5] loadUser(currentUser.id).kurs = file[5:-5]
db.session.commit() db.session.commit()
else: else:
return redirect(url_for("error", ecode=900)) return redirect(url_for("error", ecode=900))
return redirect(url_for("welcome")) return redirect(url_for("welcome"))
@app.route("/log-in") @app.route("/log-in")
def login(): def login():
""" """
@ -253,6 +259,32 @@ def login():
""" """
return render_template("login.html", theorie="hidden", praxis="hidden", s="s") 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()
except IntegrityError:
return redirect(url_for("displayRapla", week=day))
await createCustomCalendar(currentUser.id)
return redirect(url_for("displayRapla", week=day))
@app.route("/plan/restore/event")
@loginRequired
async def restoreEvent():
id = str(request.args.get("id"))
uid = currentUser.id
entry = HiddenVL.query.filter_by(id=str(uid) + id).first()
db.session.delete(entry)
db.session.commit()
await createCustomCalendar(currentUser.id)
return redirect(url_for("restoreVL"))
@app.route("/log-in", methods=["POST"]) @app.route("/log-in", methods=["POST"])
async def login_post(): async def login_post():
@ -264,93 +296,95 @@ async def login_post():
""" """
email = request.form.get("email") email = request.form.get("email")
password = request.form.get("password") password = request.form.get("password")
n = request.args.get("next") nextArg = request.args.get("next")
if n: if nextArg:
success = make_response(redirect(n)) success = make_response(redirect(nextArg))
else: else:
success = make_response(redirect(url_for("getKurs"))) success = make_response(redirect(url_for("getKurs")))
user = User.query.filter_by(email=email).first() user = User.query.filter_by(email=email).first()
t = await fetchDUALIS.checkUser(email, password) tokenAndCookie = await fetchDUALIS.checkUser(email, password)
if t[0] == -2: if tokenAndCookie[0] == -2:
return redirect(url_for("login", code=-2)) return redirect(url_for("login", code=-2))
if user: if user:
dualis = Dualis.query.filter_by(uid=user.id).first() dualisUser = Dualis.query.filter_by(uid=user.id).first()
dualis.token = t[0] dualisUser.token = tokenAndCookie[0]
newcookie = t[1] newCookie = tokenAndCookie[1]
dualis.token_created = time.time() dualisUser.token_created = time.time()
db.session.commit() db.session.commit()
login_user(user) loginUser(user)
if user.kurs: if user.kurs:
if not dualis.semester: if not dualisUser.semester:
success = make_response(redirect(url_for("getSemester"))) success = make_response(redirect(url_for("getSemester")))
elif not n: elif not nextArg:
success = make_response(redirect(url_for("welcome"))) success = make_response(redirect(url_for("welcome")))
success.set_cookie("cnsc", value=newcookie, httponly=True, secure=True) success.set_cookie("cnsc", value=newCookie, httponly=True, secure=True)
else: else:
hashid = int(hashlib.sha1(email.encode("utf-8")).hexdigest(), 16) % (10 ** 8) hashedID = int(hashlib.sha1(email.encode("utf-8")).hexdigest(), 16) % (10 ** 8)
pname = email.find(".") + 1 vorname = email.find(".") + 1
ename = min(email[pname:].find("."), email[pname:].find("@")) nachname = min(email[vorname:].find("."), email[vorname:].find("@"))
name = email[pname:pname + ename].capitalize() name = email[vorname:(vorname + nachname)].capitalize()
new_user = User(email=email, name=name, id=hashid) new_user = User(email=email, name=name, id=hashedID)
db.session.add(new_user) db.session.add(new_user)
cookie = requesthelpers.getCookie(t[1].cookies)
new_dualis = Dualis(uid=hashid, token=t[0], token_created=int(time.time()))
db.session.add(new_dualis)
db.session.commit() db.session.commit()
login_user(new_user) cookie = tokenAndCookie[1]
newDualis = Dualis(uid=hashedID, token=tokenAndCookie[0], token_created=int(time.time()))
db.session.add(newDualis)
db.session.commit()
loginUser(new_user)
success.set_cookie("cnsc", value=cookie, httponly=True, secure=True) success.set_cookie("cnsc", value=cookie, httponly=True, secure=True)
return success return success
@app.route("/log-out") @app.route("/log-out")
@login_required @loginRequired
async def logout(): async def logout():
""" """
Loggt den User aus. Loggt den User aus.
:return Empty Token: :return Empty Token:
""" """
cookie = request.cookies.get("cnsc") cookie = request.cookies.get("cnsc")
dualis = Dualis.query.filter_by(uid=current_user.id).first() dualisUser = Dualis.query.filter_by(uid=currentUser.id).first()
await fetchDUALIS.logOut(dualis.token, cookie) await fetchDUALIS.logOut(dualisUser.token, cookie)
dualis.token = None dualisUser.token = None
db.session.commit() db.session.commit()
logout_user() logoutUser()
red = make_response(redirect(url_for("login", code=1, next=url_for("welcome")))) redirection = make_response(redirect(url_for("login", code=1, next=url_for("welcome"))))
red.set_cookie("cnsc", value="Logged out! Your temporary token " redirection.set_cookie("cnsc", value="Logged out! Your temporary token "
"on our server and the cookie on your device have been deleted.", httponly=True, "on our server and the cookie on your device have been deleted.",
httponly=True,
secure=True) secure=True)
return red return redirection
@app.route("/error") @app.route("/error")
def error(): def error():
""" """
Error Page für custom-Errors. \n Error Page für custom-Errors. \n
TODO: Funktion depreciaten. Ersetzen durch Errors auf den entsprechenden Seiten. TODO: Funktion depreciaten. Ersetzen durch Errors auf den entsprechenden Seiten.
:return: :return HTML:
""" """
error = request.args.get("ecode") errorCode = request.args.get("ecode")
if error == "900": if errorCode == "900":
msg = "Ungültige RAPLA-URL! Sicher, dass der Link zum DHBW-Rapla führt?" message = "Ungültige RAPLA-URL! Sicher, dass der Link zum DHBW-Rapla führt?"
elif error == "899": elif errorCode == "899":
msg = "Der Kalender wurde nicht gefunden! Sicher, dass der Link korrekt ist?" message = "Der Kalender wurde nicht gefunden! Sicher, dass der Link korrekt ist?"
else: else:
msg = str(error) message = str(errorCode)
return render_template('display-message.html', message=msg) return render_template('display-message.html', message=message)
@app.route("/error") @app.route("/error")
@app.errorhandler(HTTPException) @app.errorhandler(HTTPException)
def handle(e): def handle(e):
"""" """"
HTTP-Exception-Handler HTTP-Exception-Handler
:param e:
:return HTML:
""" """
return render_template('display-message.html', message=e) 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

@ -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,13 +63,27 @@
<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"]) }}>
<button>👁</button>
</form>
<form>
<button class="plusbutton">+</button>
</form>
</div> </div>
{% endif %} {% 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 %}
{% endfor %} {% endfor %}
</div> </div>
</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

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

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

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

View File

@ -1,5 +1,5 @@
[uwsgi] [uwsgi]
mount = /dualhub=routing: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