RAPLA-POC, LOGIN-Flow
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ set/
|
|||||||
VIRTUAL_ENV/
|
VIRTUAL_ENV/
|
||||||
calendars/
|
calendars/
|
||||||
.idea/
|
.idea/
|
||||||
|
calendars/list.json
|
||||||
|
|||||||
72
app.py
72
app.py
@ -5,12 +5,13 @@ from flask_login import login_user, login_required, current_user, LoginManager,
|
|||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import time
|
||||||
|
|
||||||
import dualisauth
|
import dualisauth
|
||||||
import requesthelpers
|
import requesthelpers
|
||||||
from fetchRAPLA import *
|
from fetchRAPLA import *
|
||||||
from get_mysql import get_mysql
|
from get_mysql import get_mysql
|
||||||
import time
|
from parseICAL import getWeek
|
||||||
|
|
||||||
|
|
||||||
def create():
|
def create():
|
||||||
@ -55,23 +56,42 @@ class Dualis(db.Model):
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template('index.html', headermessage='Header', message='DualHub')
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/welcome")
|
@app.route("/welcome")
|
||||||
@login_required
|
@login_required
|
||||||
def welcome():
|
def welcome():
|
||||||
d = Dualis.query.filter_by(uid=current_user.id).first()
|
kurs = current_user.kurs
|
||||||
if not current_user.kurs:
|
|
||||||
kurs = dualisauth.getKurs(d.token, d.cookie)
|
|
||||||
current_user.kurs = kurs
|
|
||||||
db.session.commit()
|
|
||||||
else:
|
|
||||||
kurs = current_user.kurs
|
|
||||||
name = current_user.name
|
name = current_user.name
|
||||||
return render_template('index.html', headermessage='DualHub', message="Hallo, "
|
return render_template('index.html', headermessage='DualHub', message="Hallo, "
|
||||||
+ name + " (" + kurs + ")")
|
+ name + " (" + kurs + ")")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/backendpoc/set-up")
|
||||||
|
@login_required
|
||||||
|
def getKurs():
|
||||||
|
d = Dualis.query.filter_by(uid=current_user.id).first()
|
||||||
|
if d:
|
||||||
|
e = False
|
||||||
|
if not current_user.kurs:
|
||||||
|
kurs = dualisauth.getKurs(d.token, d.cookie)
|
||||||
|
if kurs != 0:
|
||||||
|
current_user.kurs = kurs
|
||||||
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
e = True
|
||||||
|
else:
|
||||||
|
kurs = current_user.kurs
|
||||||
|
current_user.kurs = kurs
|
||||||
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
e = True
|
||||||
|
kurs = ""
|
||||||
|
return render_template('kurs.html', detected=(kurs, e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/backendpoc/error<int:ecode>")
|
@app.route("/backendpoc/error<int:ecode>")
|
||||||
def error(ecode):
|
def error(ecode):
|
||||||
if ecode == 900:
|
if ecode == 900:
|
||||||
@ -91,7 +111,7 @@ def chooseRaplas():
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@app.route("/backendpoc/plan", methods=["POST"])
|
@app.route("/backendpoc/rapla", methods=["POST"])
|
||||||
def getRapla():
|
def getRapla():
|
||||||
file = str(request.form.get("file"))
|
file = str(request.form.get("file"))
|
||||||
url = str(request.form.get("url"))
|
url = str(request.form.get("url"))
|
||||||
@ -100,16 +120,25 @@ def getRapla():
|
|||||||
if file != "None":
|
if file != "None":
|
||||||
User.query.filter_by(id=current_user.id).first().kurs = file[5:-5]
|
User.query.filter_by(id=current_user.id).first().kurs = file[5:-5]
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return send_file("calendars/" + file)
|
#return send_file("calendars/" + file)
|
||||||
elif url != "None":
|
elif url != "None":
|
||||||
file = getNewRapla(url)
|
file = getNewRapla(url)
|
||||||
if type(file) is not int:
|
if type(file) is not int:
|
||||||
User.query.filter_by(id=current_user.id).first().kurs = file[5:-5]
|
User.query.filter_by(id=current_user.id).first().kurs = file[5:-5]
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return send_file("calendars/" + file)
|
#return send_file("calendars/" + file)
|
||||||
else:
|
else:
|
||||||
return redirect(url_for("error", ecode=file + 900))
|
return redirect(url_for("error", ecode=file + 900))
|
||||||
return render_template("index.html")
|
return redirect(url_for("welcome"))
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@app.route("/backendpoc/plan", methods=["GET"])
|
||||||
|
def displayRapla():
|
||||||
|
samstag = request.args.get("samstag")
|
||||||
|
if not samstag:
|
||||||
|
samstag = False
|
||||||
|
events = getWeek("today", "rapla"+current_user.kurs+".ical", samstag)
|
||||||
|
return render_template("plan.html", events=events[0], eventdays=events[1])
|
||||||
|
|
||||||
|
|
||||||
@app.route("/backendpoc/log-in")
|
@app.route("/backendpoc/log-in")
|
||||||
@ -127,21 +156,19 @@ def login_post():
|
|||||||
if n:
|
if n:
|
||||||
success = redirect(n)
|
success = redirect(n)
|
||||||
else:
|
else:
|
||||||
success = redirect(url_for("welcome"))
|
success = redirect(url_for("getKurs"))
|
||||||
|
|
||||||
user = User.query.filter_by(email=email).first()
|
user = User.query.filter_by(email=email).first()
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
dualis = Dualis.query.filter_by(uid=user.id).first()
|
dualis = Dualis.query.filter_by(uid=user.id).first()
|
||||||
if check_password_hash(user.password, password):
|
if check_password_hash(user.password, password):
|
||||||
if not dualisauth.checkLifetime(dualis.token_created):
|
if not dualis.token or not dualisauth.checkLifetime(dualis.token_created):
|
||||||
new_token = dualisauth.checkUser(email, password)
|
new_token = dualisauth.checkUser(email, password)
|
||||||
dualis.token = new_token[0]
|
dualis.token = new_token[0]
|
||||||
dualis.cookie = requesthelpers.getCookie(new_token[1].cookies)
|
dualis.cookie = requesthelpers.getCookie(new_token[1].cookies)
|
||||||
dualis.token_created = time.time()
|
dualis.token_created = time.time()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
login_user(user)
|
|
||||||
return success
|
|
||||||
else:
|
else:
|
||||||
t = dualisauth.checkUser(email, password)
|
t = dualisauth.checkUser(email, password)
|
||||||
if t[0] == -2:
|
if t[0] == -2:
|
||||||
@ -152,8 +179,10 @@ def login_post():
|
|||||||
dualis.cookie = requesthelpers.getCookie(t[1].cookies)
|
dualis.cookie = requesthelpers.getCookie(t[1].cookies)
|
||||||
dualis.token_created = time.time()
|
dualis.token_created = time.time()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
login_user(user)
|
login_user(user)
|
||||||
return success
|
if user.kurs:
|
||||||
|
success = redirect(url_for("welcome"))
|
||||||
|
return success
|
||||||
|
|
||||||
t = dualisauth.checkUser(email, password)
|
t = dualisauth.checkUser(email, password)
|
||||||
if t[0] == -2:
|
if t[0] == -2:
|
||||||
@ -180,6 +209,11 @@ def login_post():
|
|||||||
|
|
||||||
@app.route("/backendpoc/log-out")
|
@app.route("/backendpoc/log-out")
|
||||||
def logout():
|
def logout():
|
||||||
|
dualis = Dualis.query.filter_by(uid=current_user.id).first()
|
||||||
|
dualisauth.logOut(dualis.token, dualis.cookie)
|
||||||
|
dualis.cookie = None
|
||||||
|
dualis.token = None
|
||||||
|
db.session.commit()
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for("login", code=1))
|
return redirect(url_for("login", code=1))
|
||||||
|
|
||||||
|
|||||||
@ -29,22 +29,33 @@ def checkUser(email: str, password: str):
|
|||||||
return token, s
|
return token, s
|
||||||
|
|
||||||
|
|
||||||
def getKurs(token: str, cookie: int):
|
def getKurs(token: int, cookie: str):
|
||||||
headers["Cookie"] = "cnsc=" + str(cookie)
|
try:
|
||||||
response = requests.request("GET",
|
headers["Cookie"] = "cnsc=" + cookie
|
||||||
url + "?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N" + token + ",-N000307,",
|
token = str(token)
|
||||||
headers=headers, data={})
|
response = requests.request("GET",
|
||||||
html = BeautifulSoup(response.text, 'lxml')
|
url + "?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N" + token + ",-N000307,",
|
||||||
link = html.body.find('a', attrs={'id': "Popup_details0001"})['href']
|
headers=headers, data={})
|
||||||
response = requests.request("GET", url+link[21:], headers=headers, data={})
|
html = BeautifulSoup(response.text, 'lxml')
|
||||||
html = BeautifulSoup(response.text, 'lxml')
|
link = html.body.find('a', attrs={'id': "Popup_details0001"})['href']
|
||||||
content = html.body.find('td', attrs={'class': 'level02'}).text
|
response = requests.request("GET", url + link[21:], headers=headers, data={})
|
||||||
start = content.find(" ")+4
|
html = BeautifulSoup(response.text, 'lxml')
|
||||||
end = start + (content[start:].find(" "))
|
content = html.body.find('td', attrs={'class': 'level02'}).text
|
||||||
kurs = content[start:end]
|
start = content.find(" ") + 4
|
||||||
|
end = start + (content[start:].find(" "))
|
||||||
|
kurs = content[start:end]
|
||||||
|
except:
|
||||||
|
kurs = 0
|
||||||
return kurs
|
return kurs
|
||||||
|
|
||||||
|
|
||||||
|
def logOut(token: int, cookie: str):
|
||||||
|
headers["Cookie"] = "cnsc=" + cookie
|
||||||
|
response = requests.request("GET", url + "?APPNAME=CampusNet&PRGNAME=LOGOUT&ARGUMENTS=-N" + str(token) + ", -N001",
|
||||||
|
headers=headers, data={})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def checkLifetime(timecode: float):
|
def checkLifetime(timecode: float):
|
||||||
if time.time() - timecode > 1800:
|
if time.time() - timecode > 1800:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import urllib.error
|
|||||||
from urllib.request import urlretrieve
|
from urllib.request import urlretrieve
|
||||||
import icalendar
|
import icalendar
|
||||||
import json
|
import json
|
||||||
|
import recurring_ical_events
|
||||||
|
|
||||||
|
|
||||||
def parseURL(url: str):
|
def parseURL(url: str):
|
||||||
@ -37,9 +38,6 @@ def getNewRapla(url: str):
|
|||||||
except urllib.error.URLError:
|
except urllib.error.URLError:
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
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+")
|
file = open("calendars/list.json", "r+")
|
||||||
jsoncal = json.load(file)
|
jsoncal = json.load(file)
|
||||||
jsoncal.update({kurs: "rapla" + kurs + ".ical"})
|
jsoncal.update({kurs: "rapla" + kurs + ".ical"})
|
||||||
|
|||||||
62
parseICAL.py
Normal file
62
parseICAL.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import icalendar
|
||||||
|
import datetime
|
||||||
|
import recurring_ical_events
|
||||||
|
|
||||||
|
|
||||||
|
def getWeek(weekstart: datetime, file: str, showsat: bool):
|
||||||
|
if weekstart == "today":
|
||||||
|
start_date = datetime.date.today()
|
||||||
|
else:
|
||||||
|
start_date = weekstart
|
||||||
|
start_date -= datetime.timedelta(days=start_date.weekday() % 7)
|
||||||
|
end_date = start_date + datetime.timedelta(days=7)
|
||||||
|
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
|
||||||
|
formstart = str(estart.hour) + ":" + str(estart.minute)
|
||||||
|
eend = event["DTEND"].dt
|
||||||
|
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]
|
||||||
|
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
|
||||||
|
}
|
||||||
|
eventl += [eventdict]
|
||||||
|
return eventl, daylist(start_date, showsat)
|
||||||
|
|
||||||
|
|
||||||
|
shortnames = ["mon", "tue", "wed", "thu", "fri", "sat"]
|
||||||
|
longnames = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]
|
||||||
|
|
||||||
|
|
||||||
|
def daylist(weekstart: datetime, showsat: bool):
|
||||||
|
weekday = weekstart
|
||||||
|
dayl = []
|
||||||
|
if showsat:
|
||||||
|
r = 6
|
||||||
|
else:
|
||||||
|
r = 5
|
||||||
|
for i in range(r):
|
||||||
|
dayl += [{
|
||||||
|
"day": weekday.day,
|
||||||
|
"short": shortnames[i],
|
||||||
|
"long": longnames[i],
|
||||||
|
}]
|
||||||
|
weekday += datetime.timedelta(days=1)
|
||||||
|
return dayl
|
||||||
133
static/cal.scss
Normal file
133
static/cal.scss
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
:root {
|
||||||
|
--numDays: 5;
|
||||||
|
--numHours: 10;
|
||||||
|
--timeHeight: 60px;
|
||||||
|
--calBgColor: #fff1f8;
|
||||||
|
--eventBorderColor: #f2d3d8;
|
||||||
|
--eventColor1: #ffd6d1;
|
||||||
|
--eventColor2: #fafaa3;
|
||||||
|
--eventColor3: #e2f8ff;
|
||||||
|
--eventColor4: #d1ffe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
margin: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(var(--numHours), var(--timeHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
.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(--numHours), var(--timeHeight));
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--calBgColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place on Timeline
|
||||||
|
.start-1000 {
|
||||||
|
grid-row-start: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-1200 {
|
||||||
|
grid-row-start: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-1300 {
|
||||||
|
grid-row-start: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-1400 {
|
||||||
|
grid-row-start: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-1200 {
|
||||||
|
grid-row-end: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-1300 {
|
||||||
|
grid-row-end: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-1500 {
|
||||||
|
grid-row-end: 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-1600 {
|
||||||
|
grid-row-end: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-1700 {
|
||||||
|
grid-row-end: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
border: 1px solid var(--eventBorderColor);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space,
|
||||||
|
.date {
|
||||||
|
height: 60px
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global / Etc
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corp-fi {
|
||||||
|
background: var(--eventColor1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ent-law {
|
||||||
|
background: var(--eventColor2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.writing {
|
||||||
|
background: var(--eventColor3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.securities {
|
||||||
|
background: var(--eventColor4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-num {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-day {
|
||||||
|
display: inline;
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ body
|
|||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100px;
|
width: 150px;
|
||||||
height: 50px
|
height: 50px
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
templates/kurs.html
Normal file
22
templates/kurs.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Kurs wählen</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
<script async src="https://analytics.paulmartin.cloud/script.js" data-website-id="459fa66e-e255-4393-8e89-ead8b1572d0d"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% if not detected[1] %}
|
||||||
|
<h1>Wir haben {{ detected[0] }} als deinen Kurs ermittelt. Falls er nicht stimmt, kannst du ihn unten auswählen.</h1>
|
||||||
|
<form action={{ url_for("welcome") }}>
|
||||||
|
<input type="submit" value="Der Kurs stimmt!">
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<h1>Dein Kurs konnte leider nicht ermittelt werden. Klicke den Button, um direkt zur Auswahl zu kommen.</h1>
|
||||||
|
{% endif %}
|
||||||
|
<form action={{ url_for("chooseRaplas") }}>
|
||||||
|
<input type="submit" value="Manuell auswählen!">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
51
templates/plan.html
Normal file
51
templates/plan.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<!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') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="events">
|
||||||
|
{% for i in events %}
|
||||||
|
{% if i["weekday"] == e %}
|
||||||
|
<div class="event start-{{ i["start"][:2]+i["start"][3:] }} end-{{ i["end"][:2]+i["end"][3:]}}">
|
||||||
|
<p class="title">{{ i["name"] }}</p>
|
||||||
|
<p class="room">{{ i["room"] }}</p>
|
||||||
|
<p class="duration">{{ i["dur"] }}</p>
|
||||||
|
<p class="time"> {{ i["start"] }} - {{ i["end"] }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user