68 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
56ec2cfb15 Refreshs 2024-04-02 17:58:30 +02:00
4260ca0e92 Mensa-Fix 2024-01-22 18:10:41 +01:00
3b8cf7e53d Nested Noten 2024-01-11 14:50:28 +01:00
b8c2bea4ae Content Security Policy 2023-12-15 15:19:26 +01:00
937bf9787c Analytics-Link 2023-12-15 10:18:37 +01:00
c77f6017d4 Quickfix Rapla-Pipeline 2023-12-15 09:55:40 +01:00
8ddd7a972c Navbar-Hover + Analytics 2023-12-15 09:29:59 +01:00
1c971975dd Plan-Buttons 2023-12-14 22:29:39 +01:00
2d964e0a29 Plan-Animation 2023-12-14 22:25:19 +01:00
4f9fb5ac4d Quickfix Plan-Anon 2023-12-14 22:11:51 +01:00
fc97411177 Errorpage + RAPLA-Styling 2023-12-14 21:43:13 +01:00
a6372086db Launch prep 2023-12-14 18:49:45 +01:00
64115fe2d0 Merge remote-tracking branch 'origin/Frontend' into Backend
# Conflicts:
#	static/style.css
#	templates/index.html
2023-12-14 13:13:12 +01:00
07d4dd098f Dasboard 2023-12-14 13:10:22 +01:00
65d14f51cd Dashboard 2023-12-14 13:09:36 +01:00
15954af651 Plan + Tests 2023-12-13 18:52:59 +01:00
ae8b0206fc Plan prep frontend 2023-12-12 23:43:29 +01:00
b302ce4d49 Plan prep backend 2023-12-12 23:29:40 +01:00
3a3b6c895c plan.html Quickfix 2023-12-11 15:14:43 +01:00
c1959b9a84 Merge remote-tracking branch 'origin/Backend' into Backend 2023-12-11 15:11:44 +01:00
e3cab5cd43 Timeout-Check + MERGE-Prep 2023-12-11 15:11:29 +01:00
6c143b5817 Timeout-Check + MERGE-Prep 2023-12-11 14:37:23 +01:00
081cb4aaa7 DUALIS Noten POC 2023-12-08 11:20:00 +01:00
1b69dc7085 Quick-Fix Mensa id-gen 2023-12-04 22:12:34 +01:00
b07438fcfb Quick-Fix Mensa cronjob 2023-12-04 21:54:53 +01:00
6d26190fed Fix Mensa cronjob 2023-12-04 21:48:44 +01:00
dbcf3cc0ce refresh Mensa 2023-12-04 21:42:58 +01:00
96a116e240 Set Cookies httpOnly, secure; Deployment properties 2023-12-01 00:01:03 +01:00
8af85d1abd QUICK FIX DEPLOYMENT 2023-11-30 23:44:52 +01:00
b7174f353c Dont store cookies + iCal refresh 2023-11-30 22:40:03 +01:00
40fde2445c Mensa Integration 2023-11-29 15:04:28 +01:00
315d882502 implement talisman 2023-11-29 13:12:17 +01:00
c5e143b4f7 Merge remote-tracking branch 'origin/Backend' into Backend 2023-11-29 11:16:24 +01:00
c373fa1428 RAPLA-POC, LOGIN-Flow 2023-11-29 11:15:49 +01:00
08d7851248 RAPLA-POC, LOGIN-Flow 2023-11-29 11:10:01 +01:00
b07b25cf47 Merge remote-tracking branch 'origin/Backend' into Backend
# Conflicts:
#	app.py
#	dualisauth.py
2023-11-24 12:04:05 +01:00
d8d1a77f18 get-kurs 2023-11-24 12:01:31 +01:00
43 changed files with 4181 additions and 471 deletions

2
.gitignore vendored
View File

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

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)

189
app.py
View File

@ -1,189 +0,0 @@
#!/usr/bin/env python3.6
import requests
from flask import Flask
from flask import render_template, url_for, send_from_directory, redirect, request, send_file
from flask_login import login_user, login_required, current_user, LoginManager, UserMixin, logout_user
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
import hashlib
import dualisauth
import requesthelpers
from fetchRAPLA import *
from get_mysql import get_mysql
import time
def create():
app = Flask(__name__)
dbpw = get_mysql()[1]
dbun = get_mysql()[0]
app.config['SECRET_KEY'] = 'SECRET_KEY_GOES_HERE'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://' + dbun + ':' + dbpw + '@localhost/paulmrtn_DUALHUB'
db.init_app(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "login"
@login_manager.user_loader
def load_user(uid: int):
return User.query.filter_by(id=uid).first()
return app
db = SQLAlchemy()
app = create()
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
name = db.Column(db.String(255))
kurs = db.Column(db.String(15))
class Dualis(db.Model):
token = db.Column(db.String(255), unique=True)
uid = db.Column(db.Integer, primary_key=True)
token_created = db.Column(db.Integer)
result_lists = db.Column(db.String(255))
cookie = db.Column(db.String(255))
@app.route("/")
def index():
return render_template('index.html', headermessage='Header', message='DualHub')
@app.route("/welcome")
@login_required
def welcome():
d = Dualis.query.filter_by(uid=current_user.id).first()
if not d.kurs:
kurs = dualisauth.getKurs(d.token, d.cookie)
current_user.kurs = kurs
db.session.commit()
else:
kurs = d.kurs
name = current_user.name
return render_template('index.html', headermessage='DualHub', message="Hallo, "
+ name + " (" + kurs + ")")
@app.route("/backendpoc/error<int:ecode>")
def error(ecode):
if ecode == 900:
msg = "Ungültige RAPLA-URL! Sicher, dass der Link zum DHBW-Rapla führt?"
elif ecode == 899:
msg = "Der Kalender wurde nicht gefunden! Sicher, dass der Link korrekt ist?"
else:
msg = "Unbekannter Fehler!"
return render_template('index.html', message=msg, headermessage="DualHub")
@app.route("/backendpoc/rapla")
@login_required
def chooseRaplas():
r = getRaplas()
return render_template("rapla.html", raplas=r)
@login_required
@app.route("/backendpoc/plan", methods=["POST"])
def getRapla():
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()
return send_file("calendars/" + file)
elif url != "None":
file = getNewRapla(url)
if type(file) is not int:
User.query.filter_by(id=current_user.id).first().kurs = file[5:-5]
db.session.commit()
return send_file("calendars/" + file)
else:
return redirect(url_for("error", ecode=file + 900))
return render_template("index.html")
@app.route("/backendpoc/log-in")
def login(code: int = None):
if code:
print(code)
return render_template("login.html")
@app.route("/backendpoc/log-in", methods=["POST"])
def login_post():
email = request.form.get("email")
password = request.form.get("password")
n = request.args.get("next")
if n:
success = redirect(n)
else:
success = redirect(url_for("welcome"))
user = User.query.filter_by(email=email).first()
if user:
dualis = Dualis.query.filter_by(uid=user.id).first()
if check_password_hash(user.password, password):
if not dualisauth.checkLifetime(dualis.token_created):
new_token = dualisauth.checkUser(email, password)
dualis.token = new_token[0]
dualis.cookie = requesthelpers.getCookie(new_token[1].cookies)
dualis.token_created = time.time()
db.session.commit()
login_user(user)
return success
else:
t = dualisauth.checkUser(email, password)
if t[0] == -2:
return redirect(url_for("login", code=-2))
else:
user.password = generate_password_hash(password, method="pbkdf2:sha256")
dualis.token = t[0]
dualis.cookie = requesthelpers.getCookie(t[1].cookies)
dualis.token_created = time.time()
db.session.commit()
login_user(user)
return success
t = dualisauth.checkUser(email, password)
if t[0] == -2:
return redirect(url_for("login", code=-2))
hashid = int(hashlib.sha1(email.encode("utf-8")).hexdigest(), 16) % (10 ** 8)
hashpw = generate_password_hash(password, method="pbkdf2:sha256")
pname = email.find(".") + 1
ename = min(email[pname:].find("."), email[pname:].find("@"))
name = email[pname:pname + ename].capitalize()
new_user = User(email=email, password=hashpw, name=name, id=hashid)
db.session.add(new_user)
cookie = requesthelpers.getCookie(t[1].cookies)
new_dualis = Dualis(uid=hashid, token=t[0], token_created=int(time.time()), cookie=cookie)
db.session.add(new_dualis)
db.session.commit()
login_user(new_user)
return success
@app.route("/backendpoc/log-out")
def logout():
logout_user()
return redirect(url_for("login", code=1))
if __name__ == "__main__":
app.run(host='0.0.0.0', port=2024, debug=True)

136
calendar_generation.py Normal file
View File

@ -0,0 +1,136 @@
import time
import asyncio
import icalendar
import datetime
import recurring_ical_events
from fetchMENSA import getMeals
import pytz
from init import *
shortnames = ["mon", "tue", "wed", "thu", "fri", "sat"]
longnames = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]
months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober",
"November", "Dezember"]
async def getWeek(weekstart: datetime, file: str, showsat: bool):
"""
Liefert alle Events einer Woche zurück. \n
Wochenstart wird automatisch auf den Montag der Woche gelegt. \n
:param weekstart:
:param file:
:param showsat:
:return (Event-Liste, Essens-Liste, voheriges Wochendatum, nächstes Wochendatum, Datum des Montags):
"""
if weekstart == "today":
start_date = datetime.date.today()
else:
start_date = weekstart
nextw = start_date + datetime.timedelta(weeks=1)
prevw = start_date - datetime.timedelta(weeks=1)
start_date -= datetime.timedelta(days=start_date.weekday() % 7)
end_date = start_date + datetime.timedelta(days=6)
mon = months[start_date.month - 1]
if start_date.month != end_date.month:
if start_date.year != end_date.year:
mon += " " + str(start_date.year) + " | " + months[end_date.month - 1]
else:
mon += " | " + months[end_date.month-1]
mon += " " + str(end_date.year)
with open("calendars/" + file) as f:
calendar = icalendar.Calendar.from_ical(f.read())
events = recurring_ical_events.of(calendar).between(start_date, end_date)
eventl = []
for event in events:
estart = event["DTSTART"].dt
if str(estart.tzinfo) != "Europe/Berlin":
estart = estart.astimezone(pytz.timezone("Europe/Berlin"))
formstart = str(estart.hour) + ":" + str(estart.minute)
eend = event["DTEND"].dt
if str(eend.tzinfo) != "Europe/Berlin":
eend = eend.astimezone(pytz.timezone("Europe/Berlin"))
formend = str(eend.hour) + ":" + str(eend.minute)
forml = [formstart, formend]
for i in range(2):
if len(forml[i]) != 5:
if forml[i][-2] == ":":
forml[i] = forml[i] + "0"
if forml[i][1] == ":":
forml[i] = "0" + forml[i]
formstart = forml[0]
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 = {
"start": formstart,
"end": formend,
"dur": str(event["DTEND"].dt - event["DTSTART"].dt)[:-3],
"name": event["SUMMARY"],
"room": event["LOCATION"],
"weekday": estart.weekday(),
"day": estart.day,
"teacher": teacher,
"id": event["UID"]
}
eventl += [eventdict]
return eventl, await MensaDayList(start_date, showsat), prevw, nextw, mon
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.
:param weekstart:
:param showsat:
:return Essens-Liste:
"""
weekday = weekstart
dayl = []
if showsat:
r = 6
else:
r = 5
essen = []
for i in range(r):
essen += [getMeals(weekday)]
dayl += [{
"day": weekday.day,
"short": shortnames[i],
"long": longnames[i],
"mensa": i
}]
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

View File

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

View File

@ -1 +1 @@
25454 18291

View File

@ -1,52 +0,0 @@
import requests
import urllib.parse
import time
from bs4 import BeautifulSoup
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'
}
url = "https://dualis.dhbw.de/scripts/mgrqispi.dll"
def checkUser(email: str, password: str):
s = requests.Session()
fpw = urllib.parse.quote(password, safe='', encoding=None, errors=None)
fmail = urllib.parse.quote(email, safe='', encoding=None, errors=None)
payload = 'usrname=' + fmail + '&pass=' + fpw + '&ARGUMENTS=clino%2Cusrname%2Cpass%2Cmenuno%2Cmenu_type%2Cbrowser%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]
return token, s
def getKurs(token: str, cookie: int):
headers["Cookie"] = "cnsc=" + str(cookie)
response = requests.request("GET",
url + "?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N" + token + ",-N000307,",
headers=headers, data={})
html = BeautifulSoup(response.text, 'lxml')
link = html.body.find('a', attrs={'id': "Popup_details0001"})['href']
response = requests.request("GET", url+link[21:], headers=headers, data={})
html = BeautifulSoup(response.text, 'lxml')
content = html.body.find('td', attrs={'class': 'level02'}).text
start = content.find(" ")+4
end = start + (content[start:].find(" "))
kurs = content[start:end]
return kurs
def checkLifetime(timecode):
if time.time() - timecode > 1800:
return False
else:
return True

199
fetchDUALIS.py Normal file
View File

@ -0,0 +1,199 @@
import urllib.parse
import time
from bs4 import BeautifulSoup
from flask import redirect, url_for
from init import Dualis
import asyncio
import httpx
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"
async def checkUser(email: str, password: str):
"""
Erhält von Dualis den Token und Cookie für User.
:param email:
:param password:
:return (Token, Cookie):
"""
async with httpx.AsyncClient() as s:
formattedPassword = urllib.parse.quote(password, safe='', encoding=None, errors=None)
formattedEmail = urllib.parse.quote(email, safe='', encoding=None, errors=None)
content = (f"usrname={formattedEmail}&pass={formattedPassword}&ARGUMENTS=clino%2Cusrname%2Cpass%2C"
f"menuno%2Cmenu_type%2Cbrowser%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
async def getKurs(token: int, cookie: str):
"""
Bestimmt aus der ersten Prüfung den Kursbezeichner des Users.
TODO: Umstellen auf Bezeichner INKL. Standort
:param token:
:param cookie:
:return Kurs-Bezeichner ODER 0 bei Fehler:
"""
try:
headers["Cookie"] = "cnsc=" + cookie
token = str(token)
async with httpx.AsyncClient() as s:
response = await s.get(url=f"{url}?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N{token},-N000307,",
headers=headers)
html = BeautifulSoup(response.text, 'lxml')
try:
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)
html = BeautifulSoup(response.text, 'lxml')
content = html.body.find('td', attrs={'class': 'level02'}).text
start = content.find(" ") + 4
end = start + (content[start:].find(" "))
kurs = content[start:end]
except AttributeError:
kurs = 0
return kurs
async def logOut(token: int, cookie: str):
"""
Invalidiert Token und Cookie bei Dualis.
:param token:
:param cookie:
"""
headers["Cookie"] = "cnsc=" + cookie
async with httpx.AsyncClient() as s:
await s.get(url=f"{url}?APPNAME=CampusNet&PRGNAME=LOGOUT&ARGUMENTS=-N{token}, -N001", headers=headers)
async def getSem(token: int, cookie: str):
"""
Liefert die Liste aller auf Dualis verfügbaren Semester.
:param token:
:param cookie:
:return Liste [[Semester, Semester-ID], ...]:
"""
headers["Cookie"] = "cnsc=" + cookie
token = str(token)
async with httpx.AsyncClient() as s:
response = await s.get(url=f"{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:
text = i.text.replace("Wi", "Winter").replace("So", "Sommer")
text = text.replace("Se", "semester")
optlist += [[text, i['value']]]
return optlist
async def getResults(token, cookie: str, resl: str):
"""
Liefert die Liste aller Prüfungsergebnisse eines Semesters.
:param token:
:param cookie:
:param resl:
:return [[Name, Note, Credits], ...]:
"""
headers["Cookie"] = "cnsc=" + cookie
async with httpx.AsyncClient() as s:
response = await s.get(
url=f"{url}?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N{token},-N000307,,-N{resl}",
headers=headers)
html = BeautifulSoup(response.content.decode("utf-8"), 'lxml')
table = html.find('table', attrs={"class": "nb list"})
body = table.find("tbody")
vorlesungen = body.find_all("tr")
vorlesungenList = []
tasks = []
i = 0
for row in vorlesungen:
columns = row.find_all("td")
column = [[e.text.strip()] for e in columns]
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]
async def getPruefung(s, url):
"""
Ermittelt Noten "geschachtelter" Prüfungen, die nicht auf der Hauptseite angezeigt werden.
TODO: Namen der spezifischen Prüfungen auch zurückgeben, um Zusammensetzung zu spezifizieren.
:param s:
:param url:
:return Noten-Liste:
"""
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")
returnList = []
for row in pruefung:
columns = row.find_all("td")
column = [e.text.strip() for e in columns]
if len(column) == 6 and len(column[3]) <= 13:
if len(column[3]) != 0:
returnList += [column[0] + ": " + column[3].split("\xa0")[0]]
if returnList[-1][0] == ':':
returnList[-1] = "Gesamt" + returnList[-1]
if len(returnList) == 0:
returnList = ["Noch nicht gesetzt"]
return returnList
def checkLifetime(timecode: float):
"""
Dualis-Token laufen nach 30 Minuten ab.
True, wenn Token noch gültig ist.
False wenn ungültig.
:param timecode:
:return:
"""
if time.time() - timecode > 1800:
return False
else:
return True
def timeOut(dualis: Dualis, cookie: str, origin: str):
"""
Checkt, ob Token und Cookie noch valide/vorhanden sind.
False, falls alles richtig ist.
Weiterleitung zum Login, falls nicht.
:param dualis:
:param cookie:
:param origin:
:return:
"""
if not checkLifetime(dualis.token_created) or not cookie:
return redirect(url_for("login", next=url_for(origin), code=10))
else:
return False

151
fetchMENSA.py Normal file
View File

@ -0,0 +1,151 @@
import json
import asyncio
from init import db, Meals, scheduler, flaskApp
import datetime
import time
import httpx
nomeal = 'Essen nicht (mehr) verfügbar'
async def getMeals(day: datetime):
"""
Liefert alle Mahlzeiten eines Tages. \n
Befinden sie sich schon in der Datenbank, werden diese zurückgegeben. \n
Wenn nicht, wird getMealsFromAPI() aufgerufen. \n
:param day:
:return [Name1, Name2, ...]:
"""
day = formatDay(day)
essen = []
query = Meals.query.filter_by(date=day).all()
if len(query) != 0:
for i in query:
essen += [i.name]
essen.sort(key=len, reverse=True)
return essen
return await getMealsFromAPI(day, dbentry=True)
async def getMealsFromAPI(day: str, dbentry: bool = False):
"""
Fragt die Mensa-API nach den Mahlzeiten eines Tages ab. \n
Wenn dbentry: Schreibt die Ergebnisse in die Datenbank. \n
TODO: Andere Mensen berücksichtigen.
:param day:
:param dbentry:
:return [Name1, Name2, ...]:
"""
async with httpx.AsyncClient() as s:
response = await s.get(url=f"https://dh-api.paulmartin.cloud/plans/{day}?canteens=erzberger")
response = response.content
jsonResponse = json.loads(response.decode("utf-8"))
essen = []
try:
number = len(jsonResponse["data"][0]["lines"])
for i in range(number):
try:
jsonMeals = jsonResponse["data"][0]["lines"][i]["meals"]
hasContent = True
except IndexError:
essen = []
hasContent = False
if hasContent:
for e in range(len(jsonMeals)):
jsonEntry = jsonMeals[e]
name = jsonEntry["name"]
if pricetofloat(jsonEntry["price"]) >= 1.1:
vegan = jsonEntry["classifiers"].count("VG") == 1
schwein = jsonEntry["classifiers"].count("S") == 1
if vegan:
vegetarian = True
else:
vegetarian = jsonEntry["classifiers"].count("VEG") == 1
if vegetarian:
if name.count("Reibekäse") > 0:
vegan = True
essen += [name]
if dbentry:
mid = int(time.time() * 1000) % 100000
neu = Meals(date=day, name=name, id=mid, vegan=vegan, vegetarian=vegetarian,
schwein=schwein)
db.session.add(neu)
db.session.commit()
if not essen:
essen = [nomeal]
except KeyError:
essen = [nomeal]
return essen
def pricetofloat(price: str):
"""
Konvertiert den Preis-String der Mensa-API zu einem Python-Float.
:param price:
:return float:
"""
price = price[:-2]
price = price.replace(",", ".")
try:
return float(price)
except ValueError:
return 0
def formatDay(day: datetime):
"""
Füllt ein Datum mit Nullen auf. \n
"2023-1-1""2023-01-01".
:param day:
:return str:
"""
if day.month < 10:
monat = "0" + str(day.month)
else:
monat = str(day.month)
if day.day < 10:
tag = "0" + str(day.day)
else:
tag = str(day.day)
formattedDay = str(day.year) + "-" + monat + "-" + tag
return formattedDay
async def refreshMeals():
"""
Aktualisiert alle Mahlzeiten in der Datenbank. \n
Datenbankeinträge werden ersetzt, wenn die API andere Mahlzeiten liefert.
"""
print("Aktualisiere Essenspläne...\n")
table = Meals.query.all()
dates = []
for i in table:
if i.date not in dates:
dates += [i.date]
for i in range(len(dates)):
dates[i] = formatDay(dates[i])
for i in dates:
apiNames = await getMealsFromAPI(i)
dbMeals = Meals.query.filter_by(date=i).all()
dbNames = []
for meal in dbMeals:
dbNames += [meal.name]
if set(dbNames) != set(apiNames) and nomeal not in apiNames:
for name in dbNames:
db.session.delete(Meals.query.filter_by(date=i, name=name).first())
db.session.commit()
await getMealsFromAPI(i, True)
@scheduler.task('cron', id="mensaSchedule", hour='8-11', day_of_week='*', minute='*/15', week='*', second='5')
def mensaSchedule():
"""
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,10 +1,61 @@
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
from init import scheduler, flaskApp, Rapla, db, HiddenVL, User
from calendar_generation import createCustomCalendar
def parseURL(url: str): 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)
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
In: https; page=ical
:param url:
:return [Status: int, URL: str]:
"""
rapla = url.find("rapla.") rapla = url.find("rapla.")
if rapla == -1: if rapla == -1:
return 0 return 0
@ -12,49 +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, 0
async def getNewRapla(url: str, testing=False):
"""
Speichert den iCal eines Raplas auf dem Server. \n
Gibt Namen der Datei zurück. \n
TODO: Standort zu Dateibezeichner hinzufügen, um Konflikte zu vermeiden.
:param url:
:param opt. testing:
:return str:
"""
parsed = parseRaplaURL(url)
if testing:
assert "https" in parsed[1]
try:
assert "ical" in parsed[1]
except AssertionError:
assert "key" in parsed[1]
if parsed[0] == 0:
return 0 return 0
elif parsed[0] == 1:
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:
async with httpx.AsyncClient() as s:
response = await fetchPlan(s, url)
if testing:
assert "Vollmer" in response.text
writeToFile(f"calendars/rapla{kurs}.ical", response)
except urllib.error.URLError:
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 getNewRapla(url: str): def getIcal(uid: int | None = None, kurs: str | None = None):
url = parseURL(url) """
if url == 0: Liefert den Namen der Datei des mitgegebenen Users.
return 0 :param uid:
urlfile = url.find("file=") :param kurs:
kurs = url[urlfile + 5:].upper() :return str:
"""
if uid:
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:
urlretrieve(url, "calendars/rapla" + kurs + ".ical") return rapla.file
except urllib.error.URLError: except AttributeError or KeyError:
return -1 return None
with open("calendars/rapla" + kurs + ".ical") as f:
calendar = icalendar.Calendar.from_ical(f.read())
# events = calendar.walk("VEVENT")
file = open("calendars/list.json", "r+")
jsoncal = json.load(file)
jsoncal.update({kurs: "rapla" + kurs + ".ical"})
file.close()
file = open("calendars/list.json", "w")
json.dump(jsoncal, file, indent=4)
return "rapla"+kurs+".ical"
def getRaplas(): def getRaplas():
file = open("calendars/list.json", "r") """
jsonf = json.load(file) Liefert alle in der Datenbank gespeicherten Raplas.
kursl = [] :return (Kursliste, Dateiliste, URL-Liste):
filel = [] """
for i in jsonf: raplas = Rapla.query.all()
kursl += [i] kursList = [rapla.name for rapla in raplas]
filel += [jsonf[i]] fileList = [rapla.file for rapla in raplas]
return sorted(kursl), sorted(filel) urlList = [rapla.link for rapla in raplas]
return kursList, fileList, urlList
async def refreshRapla():
"""
Aktualisiert alle gespeicherten Raplas.
"""
fileList = getRaplas()[1]
urlList = getRaplas()[2]
jobList = []
async with httpx.AsyncClient() as s:
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()

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,13 +0,0 @@
import getpass
def get_mysql():
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,3 +0,0 @@
#!/bin/sh
mysql -e "USE paulmrtn_DUALHUB; CREATE TABLE user ( id int NOT NULL, email VARCHAR(255), password VARCHAR(255), name VARCHAR(255), PRIMARY KEY (ID), UNIQUE (ID, EMAIL) );"

124
init.py Normal file
View File

@ -0,0 +1,124 @@
from flask import Flask
from flask_login import LoginManager, UserMixin
from flask_sqlalchemy import SQLAlchemy
from flask_talisman import Talisman
from sqlalchemy import ForeignKey
import sys
from getMySQL import getMySQL
import atexit
from flask_apscheduler import APScheduler
def create(testing: bool = False):
"""
Erstellt die Flask-App inkl. Datenbank und Login-Manager.
:return app:
"""
app = Flask(__name__)
dbpw = getMySQL()[1]
dbun = getMySQL()[0]
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'
db.init_app(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "login"
# Shut down the scheduler when exiting the app
if not testing:
atexit.register(lambda: scheduler.shutdown())
@login_manager.user_loader
def load_user(uid: int):
return User.query.filter_by(id=uid).first()
return app
db = SQLAlchemy()
class User(UserMixin, db.Model):
"""
Datenbank-Modell für User.
"""
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
name = db.Column(db.String(255))
kurs = db.Column(db.String(15))
class Dualis(db.Model):
"""
Datenbank-Modell für Dualis.
"""
__tablename__ = 'dualis'
token = db.Column(db.String(255), unique=True)
uid = db.Column(db.Integer, ForeignKey('user.id', ondelete='CASCADE'), primary_key=True)
token_created = db.Column(db.Integer)
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):
"""
Datenbank-Modell für Meals.
"""
__tablename__ = 'meals'
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.Date)
name = db.Column(db.String(200))
vegetarian = db.Column(db.Boolean)
vegan = db.Column(db.Boolean)
schwein = db.Column(db.Boolean)
scheduler = APScheduler()
flaskApp = create()
with flaskApp.app_context():
print("Creating Tables....")
db.create_all()
def_src = ["*.paulmartin.cloud", '\'self\'']
Talisman(flaskApp, content_security_policy={"default-src": def_src, "script-src": def_src # + ["'unsafe-inline'"]
})
scheduler.init_app(flaskApp)
scheduler.start()
scheduler.api_enabled = True

View File

@ -1,8 +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.
:param cookies:
: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()

24
requirements.txt Normal file
View File

@ -0,0 +1,24 @@
beautifulsoup4~=4.12.2
Flask~=3.0.3
Flask_APScheduler
Flask_Login
flask_sqlalchemy
icalendar~=5.0.11
recurring_ical_events
talisman
Werkzeug~=3.0.0
lxml
bs4~=0.0.1
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

390
routing.py Normal file
View File

@ -0,0 +1,390 @@
#!/usr/bin/env python3.6
from flask import make_response
from flask import render_template, url_for, redirect, request, send_from_directory
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
import hashlib
import datetime
import time
import fetchDUALIS
import fetchRAPLA
from requesthelpers import *
from fetchRAPLA import *
from calendar_generation import getWeek, createCustomCalendar
from init import *
def initRoutes(app: Flask):
"""
Initialisiert die App-Routen. Nötig für Tests.
:param app:
"""
@app.route("/")
def index():
"""
Leitet den normalen Website-Aufruf zum Login weiter.
:return HTML:
"""
return redirect(url_for("login"))
@app.route("/dashboard")
@loginRequired
def welcome():
"""
Dashboard
:return HTML:
"""
if not currentUser.kurs:
return redirect(url_for("getKurs", next=url_for(request.endpoint)))
selectedPhase = request.args.get("sel")
if not selectedPhase:
selectedPhase = "theorie"
kurs = currentUser.kurs
name = currentUser.name
if selectedPhase == "theorie":
theorie = ""
praxis = "hidden"
else:
theorie = "hidden"
praxis = ""
return render_template('dashboard.html', kurs=kurs, name=name, theorie=theorie, praxis=praxis)
@app.route("/theorie/noten", methods=["GET", "POST"])
@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("/plan", methods=["GET"])
@loginRequired
async def displayRapla():
"""
Zeigt den Stundenplan für eingeloggte User an. \n
TODO: Persönliche Filter, Notizen, Essensvorlieben etc. berücksichtigen.
:return HTML:
"""
if not currentUser.kurs:
return redirect(url_for("getKurs", next=url_for(request.endpoint)))
week = request.args.get("week")
if week and week!="today":
week = datetime.strptime(week, "%Y-%m-%d")
else:
week = "today"
samstag = request.args.get("samstag")
if not samstag:
samstag = False
events = await getWeek(week, fetchRAPLA.getIcal(uid=currentUser.id), samstag)
return render_template("plan-user.html", events=events[0], eventdays=events[1],
uid=currentUser.id, name=currentUser.name, prev=str(events[2])[:10],
next=str(events[3])[:10], mon=events[4], login=True, s="p", date=str(week)[:10],
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("/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")
def redKurs():
"""
Setup beginnt mit Kurs.
:return HTML:
"""
return redirect(url_for("getKurs"))
@app.route("/set-up/kurs")
@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")
semesterList = Semesterlist.query.filter_by(uid=currentUser.id).all()
if not semesterList:
semester = await semesterDualisToDB([], token, cookie)
else:
semester = await getSemesterList(currentUser.id, token, cookie)
return render_template("semester.html", semester=semester, s="s",
theorie="hidden", praxis="hidden")
@app.route("/set-up/semester", methods=["POST"])
@loginRequired
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()
else:
return redirect(url_for("error", ecode=900))
return redirect(url_for("welcome"))
@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()
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"])
async def login_post():
"""
Verarbeitet die Eingabe von login(). \n
Falls der User schon angelegt ist, wird das Passwort verglichen. \n
Falls nicht, wird ein neuer angelegt.
:return HTML:
"""
email = request.form.get("email")
password = request.form.get("password")
nextArg = request.args.get("next")
if nextArg:
success = make_response(redirect(nextArg))
else:
success = make_response(redirect(url_for("getKurs")))
user = User.query.filter_by(email=email).first()
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]
newDualis = Dualis(uid=hashedID, token=tokenAndCookie[0], token_created=int(time.time()))
db.session.add(newDualis)
db.session.commit()
loginUser(new_user)
success.set_cookie("cnsc", value=cookie, httponly=True, secure=True)
return success
@app.route("/log-out")
@loginRequired
async def logout():
"""
Loggt den User aus.
:return Empty Token:
"""
cookie = request.cookies.get("cnsc")
dualisUser = Dualis.query.filter_by(uid=currentUser.id).first()
await fetchDUALIS.logOut(dualisUser.token, cookie)
dualisUser.token = None
db.session.commit()
logoutUser()
redirection = make_response(redirect(url_for("login", code=1, next=url_for("welcome"))))
redirection.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 redirection
@app.route("/error")
def error():
"""
Error Page für custom-Errors. \n
TODO: Funktion depreciaten. Ersetzen durch Errors auf den entsprechenden Seiten.
:return HTML:
"""
errorCode = request.args.get("ecode")
if errorCode == "900":
message = "Ungültige RAPLA-URL! Sicher, dass der Link zum DHBW-Rapla führt?"
elif errorCode == "899":
message = "Der Kalender wurde nicht gefunden! Sicher, dass der Link korrekt ist?"
else:
message = str(errorCode)
return render_template('display-message.html', message=message)
@app.route("/error")
@app.errorhandler(HTTPException)
def handle(e):
""""
HTTP-Exception-Handler
:param e:
:return HTML:
"""
return render_template('display-message.html', message=e)
if __name__ == "__main__":
initRoutes(flaskApp)
flaskApp.run(host='0.0.0.0', port=2024, debug=True)
else:
initRoutes(flaskApp)

861
static/cal.css Normal file
View File

@ -0,0 +1,861 @@
:root {
--numDays: 5;
--numHours: 13;
--numHalfHours: 23;
--numQuartHours: 45;
--timeHeight: 60px;
--halfTimeHeight: 30px;
--quartTimeHeight: 15px;
--calBgColor: #3f2d2d;
--eventBorderColor: #000000;
--eventColor1: #b82424;
--eventColor2: #fafaa3;
--eventColor3: #1894bb;
--eventColor4: rgba(184, 36, 36, 0.65);
}
.calendar {
display: grid;
gap: 10px;
grid-template-columns: auto 1fr;
margin: 2rem;
}
select {
width: 100%;
height: 40px;
border-radius: 5px;
color: white;
border-color: transparent;
background-color: var(--calBgColor);
text-align: center;
}
.userbuttons {
width: 95%;
position: absolute;
display: flex;
justify-content: end;
}
.changeweek {
font-size: 2.5em;
text-decoration: none;
font-weight: 600;
color: white;
}
.changeweek:hover {
color: transparent;
-webkit-text-fill-color: transparent;
-webkit-text-stroke: 3px white;
}
h2 {
font-weight: 300;
margin: 5px;
}
.headerblock {
display: flex;
justify-content: space-between;
padding-right: 20%;
padding-left: 20%;
}
.customheader {
display: flex;
justify-content: space-evenly;
}
a {
color: white;
font-size: 100%;
}
button {
border-radius: 5px;
border-color: white;
color: white;
border-style: solid;
background: transparent;
font-weight: 600;
width: 30px;
height: 30px;
margin: 2px;
line-height: 25px;
}
button:hover {
cursor: pointer;
}
.plusbutton {
font-size: 1.5em;
}
form {
margin-top: 5px;
}
option {
width: 300px;
font-size: 1rem;
}
.timeline {
display: grid;
grid-template-rows: repeat(12, 60px);
padding-top: 50px;
}
.days {
display: grid;
grid-column: 2;
gap: 5px;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.events {
display: grid;
grid-template-rows: repeat(var(--numQuartHours), var(--quartTimeHeight));
border-radius: 5px;
color: black;
background: var(--calBgColor);
}
.title {
width: 70%;
font-weight: 600;
margin-top: 0px;
margin-bottom: 0px;
}
.room,
.time,
.teacher {
display: block;
margin-top: 0px;
margin-bottom: 0px;
}
.room {
width: 70%;
}
.event {
border: 1px solid var(--eventBorderColor);
border-radius: 5px;
padding: 0.5rem;
margin: 0 0.5rem;
color: white;
background: var(--eventColor4);
position: relative;
}
.event:hover {
transform: scale(102%);
}
.space,
.date {
height: 100px;
}
body {
font-family: "Asap", "Calibri", "Arial", sans-serif;
color: white;
background-color: black;
}
.date {
margin-bottom: 1em;
gap: 1em;
}
.date-num {
font-size: 3rem;
font-weight: 600;
display: inline;
}
.date-day {
display: inline;
font-size: 3rem;
font-weight: 100;
}
.cs1 {
margin-left: 220px;
padding: 20px;
}
nav ul {
list-style-type: none;
margin: 0;
padding: 0;
width: 200px;
background-color: #333;
position: fixed;
top: 0;
left: 0;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
font-size: 150%;
z-index: 100;
}
nav li {
border-top: none;
}
nav li.top {
border-style: solid;
border-radius: 5px;
border-color: white;
margin: 10px;
text-align: center;
}
nav a.top:hover {
background: transparent;
}
nav li.start a {
border-top: 3px solid white;
margin-top: 20px;
padding-top: 30px;
}
nav a.selected {
color: red;
background-color: black;
}
nav li.top.selected {
border-color: red;
}
nav li a {
display: block;
color: white;
padding: 12px 16px;
text-decoration: none;
}
nav li a:hover {
background-color: red;
color: black;
}
nav li .bottom {
position: absolute;
bottom: 0;
width: 200px;
}
.space {
width: 1%;
}
.footerdiv {
width: 100%;
justify-content: right;
display: flex;
}
a.footer {
color: white;
text-decoration: none;
}
a.footer:hover {
font-weight: 600;
}
.start-0100 {
grid-row-start: 1;
}
.start-0115 {
grid-row-start: 1;
}
.start-0130 {
grid-row-start: 1;
}
.start-0145 {
grid-row-start: 1;
}
.start-0200 {
grid-row-start: 1;
}
.start-0215 {
grid-row-start: 1;
}
.start-0230 {
grid-row-start: 1;
}
.start-0245 {
grid-row-start: 1;
}
.start-0300 {
grid-row-start: 1;
}
.start-0315 {
grid-row-start: 1;
}
.start-0330 {
grid-row-start: 1;
}
.start-0345 {
grid-row-start: 1;
}
.start-0400 {
grid-row-start: 1;
}
.start-0415 {
grid-row-start: 1;
}
.start-0430 {
grid-row-start: 1;
}
.start-0445 {
grid-row-start: 1;
}
.start-0500 {
grid-row-start: 1;
}
.start-0515 {
grid-row-start: 1;
}
.start-0530 {
grid-row-start: 1;
}
.start-0545 {
grid-row-start: 1;
}
.start-0600 {
grid-row-start: 1;
}
.start-0615 {
grid-row-start: 1;
}
.start-0630 {
grid-row-start: 1;
}
.start-0645 {
grid-row-start: 1;
}
.start-0700 {
grid-row-start: 1;
}
.start-0715 {
grid-row-start: 1;
}
.start-0730 {
grid-row-start: 1;
}
.start-0745 {
grid-row-start: 1;
}
.end-0100 {
grid-row-end: 4;
}
.end-0115 {
grid-row-end: 4;
}
.end-0130 {
grid-row-end: 4;
}
.end-0145 {
grid-row-end: 4;
}
.end-0200 {
grid-row-end: 4;
}
.end-0215 {
grid-row-end: 4;
}
.end-0230 {
grid-row-end: 4;
}
.end-0245 {
grid-row-end: 4;
}
.end-0300 {
grid-row-end: 4;
}
.end-0315 {
grid-row-end: 4;
}
.end-0330 {
grid-row-end: 4;
}
.end-0345 {
grid-row-end: 4;
}
.end-0400 {
grid-row-end: 4;
}
.end-0415 {
grid-row-end: 4;
}
.end-0430 {
grid-row-end: 4;
}
.end-0445 {
grid-row-end: 4;
}
.end-0500 {
grid-row-end: 4;
}
.end-0515 {
grid-row-end: 4;
}
.end-0530 {
grid-row-end: 4;
}
.end-0545 {
grid-row-end: 4;
}
.end-0600 {
grid-row-end: 4;
}
.end-0615 {
grid-row-end: 4;
}
.end-0630 {
grid-row-end: 4;
}
.end-0645 {
grid-row-end: 4;
}
.end-0700 {
grid-row-end: 4;
}
.end-0715 {
grid-row-end: 4;
}
.end-0730 {
grid-row-end: 4;
}
.end-0745 {
grid-row-end: 4;
}
.start-0800 {
grid-row-start: 1;
}
.start-0815 {
grid-row-start: 2;
}
.start-0830 {
grid-row-start: 3;
}
.start-0845 {
grid-row-start: 4;
}
.start-0900 {
grid-row-start: 5;
}
.start-0915 {
grid-row-start: 6;
}
.start-0930 {
grid-row-start: 7;
}
.start-0945 {
grid-row-start: 8;
}
.start-1000 {
grid-row-start: 9;
}
.start-1015 {
grid-row-start: 10;
}
.start-1030 {
grid-row-start: 11;
}
.start-1045 {
grid-row-start: 12;
}
.start-1100 {
grid-row-start: 13;
}
.start-1115 {
grid-row-start: 14;
}
.start-1130 {
grid-row-start: 15;
}
.start-1145 {
grid-row-start: 16;
}
.start-1200 {
grid-row-start: 17;
}
.start-1215 {
grid-row-start: 18;
}
.start-1230 {
grid-row-start: 19;
}
.start-1245 {
grid-row-start: 20;
}
.start-1300 {
grid-row-start: 21;
}
.start-1315 {
grid-row-start: 22;
}
.start-1330 {
grid-row-start: 23;
}
.start-1345 {
grid-row-start: 24;
}
.start-1400 {
grid-row-start: 25;
}
.start-1415 {
grid-row-start: 26;
}
.start-1430 {
grid-row-start: 27;
}
.start-1445 {
grid-row-start: 28;
}
.start-1500 {
grid-row-start: 29;
}
.start-1515 {
grid-row-start: 30;
}
.start-1530 {
grid-row-start: 31;
}
.start-1545 {
grid-row-start: 32;
}
.start-1600 {
grid-row-start: 33;
}
.start-1615 {
grid-row-start: 34;
}
.start-1630 {
grid-row-start: 35;
}
.start-1645 {
grid-row-start: 36;
}
.start-1700 {
grid-row-start: 37;
}
.start-1715 {
grid-row-start: 38;
}
.start-1730 {
grid-row-start: 39;
}
.start-1745 {
grid-row-start: 40;
}
.start-1800 {
grid-row-start: 41;
}
.start-1815 {
grid-row-start: 42;
}
.start-1830 {
grid-row-start: 43;
}
.start-1845 {
grid-row-start: 44;
}
.start-1900 {
grid-row-start: 45;
}
.end-0800 {
grid-row-end: 1;
}
.end-0815 {
grid-row-end: 2;
}
.end-0830 {
grid-row-end: 3;
}
.end-0845 {
grid-row-end: 4;
}
.end-0900 {
grid-row-end: 5;
}
.end-0915 {
grid-row-end: 6;
}
.end-0930 {
grid-row-end: 7;
}
.end-0945 {
grid-row-end: 8;
}
.end-1000 {
grid-row-end: 9;
}
.end-1015 {
grid-row-end: 10;
}
.end-1030 {
grid-row-end: 11;
}
.end-1045 {
grid-row-end: 12;
}
.end-1100 {
grid-row-end: 13;
}
.end-1115 {
grid-row-end: 14;
}
.end-1130 {
grid-row-end: 15;
}
.end-1145 {
grid-row-end: 16;
}
.end-1200 {
grid-row-end: 17;
}
.end-1215 {
grid-row-end: 18;
}
.end-1230 {
grid-row-end: 19;
}
.end-1245 {
grid-row-end: 20;
}
.end-1300 {
grid-row-end: 21;
}
.end-1315 {
grid-row-end: 22;
}
.end-1330 {
grid-row-end: 23;
}
.end-1345 {
grid-row-end: 24;
}
.end-1400 {
grid-row-end: 25;
}
.end-1415 {
grid-row-end: 26;
}
.end-1430 {
grid-row-end: 27;
}
.end-1445 {
grid-row-end: 28;
}
.end-1500 {
grid-row-end: 29;
}
.end-1515 {
grid-row-end: 30;
}
.end-1530 {
grid-row-end: 31;
}
.end-1545 {
grid-row-end: 32;
}
.end-1600 {
grid-row-end: 33;
}
.end-1615 {
grid-row-end: 34;
}
.end-1630 {
grid-row-end: 35;
}
.end-1645 {
grid-row-end: 36;
}
.end-1700 {
grid-row-end: 37;
}
.end-1715 {
grid-row-end: 38;
}
.end-1730 {
grid-row-end: 39;
}
.end-1745 {
grid-row-end: 40;
}
.end-1800 {
grid-row-end: 41;
}
.end-1815 {
grid-row-end: 42;
}
.end-1830 {
grid-row-end: 43;
}
.end-1845 {
grid-row-end: 44;
}
.end-1900 {
grid-row-end: 45;
}
/*# sourceMappingURL=cal.css.map */

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

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

739
static/cal.scss Normal file
View File

@ -0,0 +1,739 @@
:root {
--numDays: 5;
--numHours: 13;
--numHalfHours: 23;
--numQuartHours: 45;
--timeHeight: 60px;
--halfTimeHeight: 30px;
--quartTimeHeight: 15px;
--calBgColor: #3f2d2d;
--eventBorderColor: #000000;
--eventColor1: #b82424;
--eventColor2: #fafaa3;
--eventColor3: #1894bb;
--eventColor4: rgba(184, 36, 36, 0.65);
}
.calendar {
display: grid;
gap: 10px;
grid-template-columns: auto 1fr;
margin: 2rem;
}
select {
width: 100%;
height: 40px;
border-radius: 5px;
color: white;
border-color: transparent;
background-color: var(--calBgColor);
text-align: center;
}
.userbuttons {
width: 95%;
position: absolute;
display: flex;
justify-content: end;
}
.changeweek {
font-size: 2.5em;
text-decoration: none;
font-weight: 600;
color: white;
}
.changeweek:hover {
color: transparent;
-webkit-text-fill-color: transparent;
-webkit-text-stroke: 3px white;
}
h2 {
font-weight: 300;
margin: 5px;
}
.headerblock {
display: flex;
justify-content: space-between;
padding-right: 20%;
padding-left: 20%;
}
.customheader {
display: flex;
justify-content: space-evenly;
}
a {
color: white;
font-size: 100%;
}
button {
border-radius: 5px;
border-color: white;
color: white;
border-style: solid;
background: transparent;
font-weight: 600;
width: 30px;
height: 30px;
margin: 2px;
line-height: 25px;
}
button:hover{
cursor: pointer;
}
.plusbutton {
font-size: 1.5em;
}
form {
margin-top: 5px;
}
option {
width: 300px;
font-size: 1rem;
}
.timeline {
display: grid;
grid-template-rows: repeat(12, 60px);
padding-top: 50px;
}
.days {
display: grid;
grid-column: 2;
gap: 5px;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.events {
display: grid;
grid-template-rows: repeat(var(--numQuartHours), var(--quartTimeHeight));
border-radius: 5px;
color: black;
background: var(--calBgColor);
}
// Events
.title {
width: 70%;
font-weight: 600;
margin-top: 0px;
margin-bottom: 0px;
}
.room,
.time,
.teacher {
display: block;
margin-top: 0px;
margin-bottom: 0px;
}
.room {
width: 70%;
}
.event {
border: 1px solid var(--eventBorderColor);
border-radius: 5px;
padding: 0.5rem;
margin: 0 0.5rem;
color: white;
background: var(--eventColor4);
position: relative;
}
.event:hover{
transform: scale(102%);
}
.space,
.date {
height: 100px
}
// Global / Etc
body {
font-family: "Asap", "Calibri", "Arial", sans-serif;
color: white;
background-color: black;
}
.date {
margin-bottom: 1em;
gap: 1em;
}
.date-num {
font-size: 3rem;
font-weight: 600;
display: inline;
}
.date-day {
display: inline;
font-size: 3rem;
font-weight: 100;
}
// Navbar
.cs1 {
margin-left: 220px;
padding: 20px;
}
nav ul {
list-style-type: none;
margin: 0;
padding: 0;
width: 200px;
background-color: #333;
position: fixed;
top: 0;
left: 0;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
font-size: 150%;
z-index: 100;
}
nav li {
border-top: none;
}
nav li.top {
border-style: solid;
border-radius: 5px;
border-color: white;
margin: 10px;
text-align: center;
}
nav a.top:hover {
background: transparent;
}
nav li.start a {
border-top: 3px solid white;
margin-top: 20px;
padding-top: 30px;
}
nav a.selected {
color: red;
background-color: black
}
nav li.top.selected {
border-color: red;
}
nav li a {
display: block;
color: white;
padding: 12px 16px;
text-decoration: none;
}
nav li a:hover {
background-color: red;
color: black;
}
nav li .bottom {
position: absolute;
bottom: 0;
width: 200px;
}
.space {
width: 1%;
}
.footerdiv {
width: 100%;
justify-content: right;
display: flex;
}
a.footer {
color: white;
text-decoration: none;
}
a.footer:hover {
font-weight: 600;
}
// Place on Timeline
// Generated by genSCSSstarts.py
.start-0100 {
grid-row-start: 1
}
.start-0115 {
grid-row-start: 1
}
.start-0130 {
grid-row-start: 1
}
.start-0145 {
grid-row-start: 1
}
.start-0200 {
grid-row-start: 1
}
.start-0215 {
grid-row-start: 1
}
.start-0230 {
grid-row-start: 1
}
.start-0245 {
grid-row-start: 1
}
.start-0300 {
grid-row-start: 1
}
.start-0315 {
grid-row-start: 1
}
.start-0330 {
grid-row-start: 1
}
.start-0345 {
grid-row-start: 1
}
.start-0400 {
grid-row-start: 1
}
.start-0415 {
grid-row-start: 1
}
.start-0430 {
grid-row-start: 1
}
.start-0445 {
grid-row-start: 1
}
.start-0500 {
grid-row-start: 1
}
.start-0515 {
grid-row-start: 1
}
.start-0530 {
grid-row-start: 1
}
.start-0545 {
grid-row-start: 1
}
.start-0600 {
grid-row-start: 1
}
.start-0615 {
grid-row-start: 1
}
.start-0630 {
grid-row-start: 1
}
.start-0645 {
grid-row-start: 1
}
.start-0700 {
grid-row-start: 1
}
.start-0715 {
grid-row-start: 1
}
.start-0730 {
grid-row-start: 1
}
.start-0745 {
grid-row-start: 1
}
.end-0100 {
grid-row-end: 4
}
.end-0115 {
grid-row-end: 4
}
.end-0130 {
grid-row-end: 4
}
.end-0145 {
grid-row-end: 4
}
.end-0200 {
grid-row-end: 4
}
.end-0215 {
grid-row-end: 4
}
.end-0230 {
grid-row-end: 4
}
.end-0245 {
grid-row-end: 4
}
.end-0300 {
grid-row-end: 4
}
.end-0315 {
grid-row-end: 4
}
.end-0330 {
grid-row-end: 4
}
.end-0345 {
grid-row-end: 4
}
.end-0400 {
grid-row-end: 4
}
.end-0415 {
grid-row-end: 4
}
.end-0430 {
grid-row-end: 4
}
.end-0445 {
grid-row-end: 4
}
.end-0500 {
grid-row-end: 4
}
.end-0515 {
grid-row-end: 4
}
.end-0530 {
grid-row-end: 4
}
.end-0545 {
grid-row-end: 4
}
.end-0600 {
grid-row-end: 4
}
.end-0615 {
grid-row-end: 4
}
.end-0630 {
grid-row-end: 4
}
.end-0645 {
grid-row-end: 4
}
.end-0700 {
grid-row-end: 4
}
.end-0715 {
grid-row-end: 4
}
.end-0730 {
grid-row-end: 4
}
.end-0745 {
grid-row-end: 4
}
.start-0800 {
grid-row-start: 1
}
.start-0815 {
grid-row-start: 2
}
.start-0830 {
grid-row-start: 3
}
.start-0845 {
grid-row-start: 4
}
.start-0900 {
grid-row-start: 5
}
.start-0915 {
grid-row-start: 6
}
.start-0930 {
grid-row-start: 7
}
.start-0945 {
grid-row-start: 8
}
.start-1000 {
grid-row-start: 9
}
.start-1015 {
grid-row-start: 10
}
.start-1030 {
grid-row-start: 11
}
.start-1045 {
grid-row-start: 12
}
.start-1100 {
grid-row-start: 13
}
.start-1115 {
grid-row-start: 14
}
.start-1130 {
grid-row-start: 15
}
.start-1145 {
grid-row-start: 16
}
.start-1200 {
grid-row-start: 17
}
.start-1215 {
grid-row-start: 18
}
.start-1230 {
grid-row-start: 19
}
.start-1245 {
grid-row-start: 20
}
.start-1300 {
grid-row-start: 21
}
.start-1315 {
grid-row-start: 22
}
.start-1330 {
grid-row-start: 23
}
.start-1345 {
grid-row-start: 24
}
.start-1400 {
grid-row-start: 25
}
.start-1415 {
grid-row-start: 26
}
.start-1430 {
grid-row-start: 27
}
.start-1445 {
grid-row-start: 28
}
.start-1500 {
grid-row-start: 29
}
.start-1515 {
grid-row-start: 30
}
.start-1530 {
grid-row-start: 31
}
.start-1545 {
grid-row-start: 32
}
.start-1600 {
grid-row-start: 33
}
.start-1615 {
grid-row-start: 34
}
.start-1630 {
grid-row-start: 35
}
.start-1645 {
grid-row-start: 36
}
.start-1700 {
grid-row-start: 37
}
.start-1715 {
grid-row-start: 38
}
.start-1730 {
grid-row-start: 39
}
.start-1745 {
grid-row-start: 40
}
.start-1800 {
grid-row-start: 41
}
.start-1815 {
grid-row-start: 42
}
.start-1830 {
grid-row-start: 43
}
.start-1845 {
grid-row-start: 44
}
.start-1900 {
grid-row-start: 45
}
.end-0800 {
grid-row-end: 1
}
.end-0815 {
grid-row-end: 2
}
.end-0830 {
grid-row-end: 3
}
.end-0845 {
grid-row-end: 4
}
.end-0900 {
grid-row-end: 5
}
.end-0915 {
grid-row-end: 6
}
.end-0930 {
grid-row-end: 7
}
.end-0945 {
grid-row-end: 8
}
.end-1000 {
grid-row-end: 9
}
.end-1015 {
grid-row-end: 10
}
.end-1030 {
grid-row-end: 11
}
.end-1045 {
grid-row-end: 12
}
.end-1100 {
grid-row-end: 13
}
.end-1115 {
grid-row-end: 14
}
.end-1130 {
grid-row-end: 15
}
.end-1145 {
grid-row-end: 16
}
.end-1200 {
grid-row-end: 17
}
.end-1215 {
grid-row-end: 18
}
.end-1230 {
grid-row-end: 19
}
.end-1245 {
grid-row-end: 20
}
.end-1300 {
grid-row-end: 21
}
.end-1315 {
grid-row-end: 22
}
.end-1330 {
grid-row-end: 23
}
.end-1345 {
grid-row-end: 24
}
.end-1400 {
grid-row-end: 25
}
.end-1415 {
grid-row-end: 26
}
.end-1430 {
grid-row-end: 27
}
.end-1445 {
grid-row-end: 28
}
.end-1500 {
grid-row-end: 29
}
.end-1515 {
grid-row-end: 30
}
.end-1530 {
grid-row-end: 31
}
.end-1545 {
grid-row-end: 32
}
.end-1600 {
grid-row-end: 33
}
.end-1615 {
grid-row-end: 34
}
.end-1630 {
grid-row-end: 35
}
.end-1645 {
grid-row-end: 36
}
.end-1700 {
grid-row-end: 37
}
.end-1715 {
grid-row-end: 38
}
.end-1730 {
grid-row-end: 39
}
.end-1745 {
grid-row-end: 40
}
.end-1800 {
grid-row-end: 41
}
.end-1815 {
grid-row-end: 42
}
.end-1830 {
grid-row-end: 43
}
.end-1845 {
grid-row-end: 44
}
.end-1900 {
grid-row-end: 45
}

7
static/dropdown.js Normal file
View File

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

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

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

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

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAAA;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;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"}

170
static/style.scss Normal file
View File

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

57
templates/dashboard.html Normal file
View File

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

View File

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

View File

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

22
templates/kurs.html Normal file
View File

@ -0,0 +1,22 @@
{% extends "index.html" %}
{% block content %}
{% if not detected[1] %}
{% if file %}
<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") %}
<form id="autoForm" action={{ url_for("getSemester") }}>
{% else %}
<form action={{ request.args.get("next") }}>
{% endif %}
<input type="submit" value="Der Kurs stimmt!">
</form>
{% else %}
<h1>Wir haben {{ detected[0] }} als deinen Kurs ermittelt. Für ihn ist aber noch kein Rapla hinterlegt.</h1>
{% endif %}
{% else %}
<h1>Dein Kurs konnte leider nicht ermittelt werden. Klicke den Button, um zur Auswahl zu kommen.</h1>
{% endif %}
<form id="manualForm" action={{ url_for("chooseRaplas", next=request.args.get("next")) }}>
<input type="submit" value="Manuell auswählen!">
</form>
{% endblock %}

View File

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

38
templates/noten.html Normal file
View File

@ -0,0 +1,38 @@
{% extends "index.html" %}
{% block head %}
<script type="text/javascript" src="{{ url_for("static", filename="dropdown.js") }}"></script>
<meta http-equiv="Refresh" content="600">
{% endblock %}
{% block content %}
{% for i in range (semester|length) %}
{% if semester[i][1]==sel %}
<h1>Deine Noten im {{ semester[i][0] }}</h1>
{% endif %}
{% endfor %}
<br>
<br>
{% for i in noten %}
{% if i[1]|length == 1 %}
{% if i[1][0][0].isnumeric() or i[1][0][-1].isalpha()%}
<h2>{{ i[0][0] }}: {{ i[1][0] }} (Credits: {{ i[2][0] }})</h2>
{% else %}
<h2>{{ i[0][0] }}: {{ i[1][0][-3:] }} (Credits: {{ i[2][0] }})</h2>
{% endif %}
{% else %}
<h2>{{ i[0][0] }} (Credits: {{ i[2][0] }}):</h2>
{% for e in i[1] %}
<h2>&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;{{ e }}</h2>
{% endfor %}
{% endif %}
{% endfor %}
<br>
<br>
<form id= "dropdown" method="post" action={{ url_for ("displayNoten") }}>
<label for="sem">Semester wählen! </label>
<select name="sem" id="sem">
{% for i in range (semester|length) %}
<option value= {{ semester [i] [1] }} {% if semester[i][1]==sel %} selected {% endif %}>{{ semester [i] [0] }} </option>
{% endfor %}
</select>
</form>
{% endblock %}

9
templates/plan-anon.html Normal file
View File

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

View File

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Vorlesungsplan</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='cal.css') }}">
<script src={{ url_for("static", filename="motd.js") }}></script>
</head>
<body>
<nav>
<ul>
<li class="top selected"><a class="top selected" href={{ url_for("welcome", sel="theorie") }}>Theorie</a></li>
<li class="top"><a class="top" href={{ url_for("welcome", sel="praxis") }}>Praxis</a></li>
<li {{ theorie }}><a href={{ url_for("displayNoten") }}>Noten</a></li>
<li {{ theorie }}><a class="selected" href={{ url_for("displayRapla") }}>Stundenplan</a></li>
<li {{ praxis }}><a href={{ url_for("redKurs") }}>To Dos</a></li>
<li><a href={{ url_for("redKurs") }}>Konfiguration</a></li>
{% block loginout %}
{% endblock %}
</ul>
</nav>
<div class="cs1">
<div class="customheader">
{% block startcontent %}
{% endblock %}
</div>
<div class="headerblock">
<a href="?week={{prev}}" class="changeweek">🢐</a>
<a href="?today" class="changeweek">{{ mon }}</a>
<a href="?week={{next}}" class="changeweek">🢒</a>
</div>
<div class="calendar">
<div class="timeline">
<div class="spacer"></div>
<div class="time-marker">08</div>
<div class="time-marker">09</div>
<div class="time-marker">10</div>
<div class="time-marker">11</div>
<div class="time-marker">12</div>
<div class="time-marker">13</div>
<div class="time-marker">14</div>
<div class="time-marker">15</div>
<div class="time-marker">16</div>
<div class="time-marker">17</div>
<div class="time-marker">18</div>
<div class="time-marker">19</div>
</div>
<div class="days">
{% for e in range (eventdays|length) %}
<div class="day {{ eventdays[e]["short"] }}">
<div class="date">
<p class="date-num">{{ eventdays[e]["day"] }}</p>
<p class="date-day">{{ eventdays[e]["long"] }}</p>
<form>
<select>
<option value="none" selected disabled> Mensa</option>
{% for n in eventdays [e]["mensa"] %}
<option class="mensa"> {{ n }}</option>
{% endfor %}
</select>
</form>
</div>
<div class="events">
{% for i in events %}
{% if i["weekday"] == e %}
{% with event=i %}
<div class="event start-{{ event["start"][:2]+event["start"][3:] }} end-{{ event["end"][:2]+event["end"][3:]}}">
{% if login %}
<div class="userbuttons">
<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>
{% 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 %}
</div>
</div>
{% endfor %}
</div>
</div>
{% block endcontent %}
{% endblock %}
</div>
</body>
</html>

12
templates/plan-user.html Normal file
View File

@ -0,0 +1,12 @@
{% extends "plan-general.html" %}
{% block loginout %}
<li><a class="bottom" href={{ url_for("logout") }}>Log-Out</a></li>
{% endblock %}
{% block startcontent %}
<h2>{{ name }}s Vorlesungsplan</h2>
{% endblock %}
{% block endcontent %}
<a href={{ url_for("deliverCalendar", uid=uid) }}>Kalender-Abo</a>
<br>
<a href={{ url_for("restoreVL") }}>Ausgeblendete Vorlesungen wieder einblenden</a>
{% endblock %}

View File

@ -1,30 +1,21 @@
<!DOCTYPE html> {% extends "index.html" %}
<html lang="de"> {% block content %}
<head>
<title> RAPLA importieren 👀</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
<script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script>
</head>
<body>
<h1>Verfügbare Raplas </h1> <h1>Verfügbare Raplas </h1>
{% block content %} <form id="dbForm" method="post" action={{ url_for ("getRapla") }}>
<form method="post" action={{ url_for ("getRapla") }}> <label for="file">Vefügbaren RAPLA wählen! </label>
<label for="file">Vefügbaren RAPLA wählen! </label>
<select name="file" id="file"> <select name="file" id="file">
<option value = "none" selected disabled hidden></option> <option value = "none" selected disabled hidden></option>
{% for i in range (raplas[0]|length) %} {% for i in range (raplas[0]|length) %}
<option value= {{ raplas [1] [i] }}>{{ raplas [0] [i] }} </option> <option value= {{ raplas [1] [i] }}>{{ raplas [0] [i] }} </option>
{% endfor %} {% endfor %}
</select> </select>
<input type="submit" value="Importieren!"> <input type="submit" value="Importieren!">
</form> </form>
{% endblock %}
<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!">
</form> </form>
</body> {% endblock %}
</html>

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 %}

16
templates/semester.html Normal file
View File

@ -0,0 +1,16 @@
{% extends "index.html" %}
{% block head %}
<script type="text/javascript" src="{{ url_for("static", filename="dropdown.js") }}"></script>
{% endblock %}
{% block content %}
<h1>Bitte wähle aus der unten stehenden Liste das Semester, dessen Noten du sehen möchtest.</h1>
<form id="dropdown" method="post" action={{ url_for ("setSemester", next=request.args.get("next")) }}>
<label for="sem">Semester wählen! </label>
<select name="sem" id="sem">
<option value = "none" selected disabled hidden></option>
{% for i in range (semester|length) %}
<option value= {{ semester [i] [1] }}>{{ semester [i] [0] }} </option>
{% endfor %}
</select>
</form>
{% endblock %}

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=app: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
@ -7,3 +7,5 @@ processes = 1
http-socket = :2024 http-socket = :2024
chmod-socket = 660 chmod-socket = 660
vacuum = true vacuum = true
enable-threads = true
thunder-lock = true