Dont store cookies + iCal refresh
This commit is contained in:
109
app.py
109
app.py
@ -1,59 +1,21 @@
|
|||||||
#!/usr/bin/env python3.6
|
#!/usr/bin/env python3.6
|
||||||
from flask import Flask
|
from flask import Flask, make_response
|
||||||
from flask import render_template, url_for, send_from_directory, redirect, request, send_file
|
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_login import login_user, login_required, current_user, LoginManager, UserMixin, logout_user, login_manager
|
||||||
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
|
||||||
from talisman import Talisman
|
from talisman import Talisman
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import datetime
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import dualisauth
|
import dualisauth
|
||||||
|
import fetchRAPLA
|
||||||
import requesthelpers
|
import requesthelpers
|
||||||
from fetchRAPLA import *
|
from fetchRAPLA import *
|
||||||
from get_mysql import get_mysql
|
from get_mysql import get_mysql
|
||||||
from parseICAL import getWeek
|
from calendar_generation import getWeek
|
||||||
|
from init import *
|
||||||
|
|
||||||
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()
|
|
||||||
Talisman(app)
|
|
||||||
|
|
||||||
|
|
||||||
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("/")
|
@app.route("/")
|
||||||
@ -77,7 +39,8 @@ def getKurs():
|
|||||||
if d:
|
if d:
|
||||||
e = False
|
e = False
|
||||||
if not current_user.kurs:
|
if not current_user.kurs:
|
||||||
kurs = dualisauth.getKurs(d.token, d.cookie)
|
cookie = request.cookies.get("cnsc")
|
||||||
|
kurs = dualisauth.getKurs(d.token, cookie)
|
||||||
if kurs != 0:
|
if kurs != 0:
|
||||||
current_user.kurs = kurs
|
current_user.kurs = kurs
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -136,13 +99,40 @@ def getRapla():
|
|||||||
@login_required
|
@login_required
|
||||||
@app.route("/backendpoc/plan", methods=["GET"])
|
@app.route("/backendpoc/plan", methods=["GET"])
|
||||||
def displayRapla():
|
def displayRapla():
|
||||||
|
week = request.args.get("week")
|
||||||
|
if week:
|
||||||
|
week = datetime.datetime.strptime(week, "%Y-%m-%d")
|
||||||
|
else:
|
||||||
|
week = "today"
|
||||||
samstag = request.args.get("samstag")
|
samstag = request.args.get("samstag")
|
||||||
if not samstag:
|
if not samstag:
|
||||||
samstag = False
|
samstag = False
|
||||||
events = getWeek("today", "rapla"+current_user.kurs+".ical", samstag)
|
events = getWeek(week, fetchRAPLA.getIcal(current_user.kurs), samstag)
|
||||||
return render_template("plan.html", events=events[0], eventdays=events[1])
|
return render_template("plan.html", events=events[0], eventdays=events[1])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/backendpoc/plan/<string:kurs>")
|
||||||
|
def displayPlan(kurs):
|
||||||
|
week = request.args.get("week")
|
||||||
|
if week:
|
||||||
|
week = datetime.datetime.strptime(week, "%Y-%m-%d")
|
||||||
|
else:
|
||||||
|
week = "today"
|
||||||
|
try:
|
||||||
|
if current_user.kurs == kurs.upper():
|
||||||
|
return redirect(url_for("displayRapla"))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
plan = fetchRAPLA.getIcal(kurs.upper())
|
||||||
|
if plan:
|
||||||
|
samstag = request.args.get("samstag")
|
||||||
|
if not samstag:
|
||||||
|
samstag = False
|
||||||
|
events = getWeek(week, plan, samstag)
|
||||||
|
return render_template("plan-anon.html", events=events[0], eventdays=events[1])
|
||||||
|
else:
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
@app.route("/backendpoc/log-in")
|
@app.route("/backendpoc/log-in")
|
||||||
def login(code: int = None):
|
def login(code: int = None):
|
||||||
if code:
|
if code:
|
||||||
@ -156,19 +146,19 @@ def login_post():
|
|||||||
password = request.form.get("password")
|
password = request.form.get("password")
|
||||||
n = request.args.get("next")
|
n = request.args.get("next")
|
||||||
if n:
|
if n:
|
||||||
success = redirect(n)
|
success = make_response(redirect(n))
|
||||||
else:
|
else:
|
||||||
success = redirect(url_for("getKurs"))
|
success = make_response(redirect(url_for("getKurs")))
|
||||||
|
|
||||||
user = User.query.filter_by(email=email).first()
|
user = User.query.filter_by(email=email).first()
|
||||||
|
newcookie = ""
|
||||||
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 dualis.token or 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)
|
newcookie = requesthelpers.getCookie(new_token[1].cookies)
|
||||||
dualis.token_created = time.time()
|
dualis.token_created = time.time()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
else:
|
else:
|
||||||
@ -178,12 +168,13 @@ def login_post():
|
|||||||
else:
|
else:
|
||||||
user.password = generate_password_hash(password, method="pbkdf2:sha256")
|
user.password = generate_password_hash(password, method="pbkdf2:sha256")
|
||||||
dualis.token = t[0]
|
dualis.token = t[0]
|
||||||
dualis.cookie = requesthelpers.getCookie(t[1].cookies)
|
newcookie = 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)
|
||||||
if user.kurs:
|
if user.kurs:
|
||||||
success = redirect(url_for("welcome"))
|
success = make_response(redirect(url_for("welcome")))
|
||||||
|
success.set_cookie("cnsc", newcookie)
|
||||||
return success
|
return success
|
||||||
|
|
||||||
t = dualisauth.checkUser(email, password)
|
t = dualisauth.checkUser(email, password)
|
||||||
@ -201,23 +192,27 @@ def login_post():
|
|||||||
|
|
||||||
cookie = requesthelpers.getCookie(t[1].cookies)
|
cookie = requesthelpers.getCookie(t[1].cookies)
|
||||||
|
|
||||||
new_dualis = Dualis(uid=hashid, token=t[0], token_created=int(time.time()), cookie=cookie)
|
new_dualis = Dualis(uid=hashid, token=t[0], token_created=int(time.time()))
|
||||||
|
|
||||||
db.session.add(new_dualis)
|
db.session.add(new_dualis)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
login_user(new_user)
|
login_user(new_user)
|
||||||
|
newcookie = cookie
|
||||||
|
success.set_cookie("cnsc", newcookie)
|
||||||
return success
|
return success
|
||||||
|
|
||||||
|
|
||||||
@app.route("/backendpoc/log-out")
|
@app.route("/backendpoc/log-out")
|
||||||
def logout():
|
def logout():
|
||||||
|
cookie = request.cookies.get("cnsc")
|
||||||
dualis = Dualis.query.filter_by(uid=current_user.id).first()
|
dualis = Dualis.query.filter_by(uid=current_user.id).first()
|
||||||
dualisauth.logOut(dualis.token, dualis.cookie)
|
dualisauth.logOut(dualis.token, cookie)
|
||||||
dualis.cookie = None
|
|
||||||
dualis.token = None
|
dualis.token = None
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for("login", code=1))
|
red = make_response(redirect(url_for("login", code=1)))
|
||||||
|
red.set_cookie("cnsc", "Logged out! Your temporary token "
|
||||||
|
"on our server and the cookie on your device have been deleted.")
|
||||||
|
return red
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
import time
|
||||||
import icalendar
|
import icalendar
|
||||||
import datetime
|
import datetime
|
||||||
import requests
|
import requests
|
||||||
import recurring_ical_events
|
import recurring_ical_events
|
||||||
import json
|
import json
|
||||||
|
from init import app, db, Meals
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def getWeek(weekstart: datetime, file: str, showsat: bool):
|
def getWeek(weekstart: datetime, file: str, showsat: bool):
|
||||||
@ -67,20 +70,49 @@ def daylist(weekstart: datetime, showsat: bool):
|
|||||||
|
|
||||||
|
|
||||||
def getMeals(day: datetime):
|
def getMeals(day: datetime):
|
||||||
|
print (day)
|
||||||
if day.day < 10:
|
if day.day < 10:
|
||||||
tag = "0" + str(day.day)
|
tag = "0" + str(day.day)
|
||||||
else:
|
else:
|
||||||
tag = str(day.day)
|
tag = str(day.day)
|
||||||
day = str(day.year) + "-" + str(day.month) + "-" + tag
|
day = str(day.year) + "-" + str(day.month) + "-" + tag
|
||||||
|
essen = []
|
||||||
|
query = Meals.query.filter_by(date=day).all()
|
||||||
|
|
||||||
|
if len(query) != 0:
|
||||||
|
for i in query:
|
||||||
|
essen += [i.name]
|
||||||
|
return essen
|
||||||
|
|
||||||
url = "https://dh-api.paulmartin.cloud/plans/" + day + "?canteens=erzberger"
|
url = "https://dh-api.paulmartin.cloud/plans/" + day + "?canteens=erzberger"
|
||||||
response = requests.request("GET", url)
|
response = requests.request("GET", url)
|
||||||
response = response.content
|
response = response.content
|
||||||
jres = json.loads(response.decode("utf-8"))
|
jres = json.loads(response.decode("utf-8"))
|
||||||
essen = []
|
|
||||||
try:
|
try:
|
||||||
num = len(jres["data"][0]["lines"])
|
num = len(jres["data"][0]["lines"])
|
||||||
for i in range(num):
|
for i in range(num):
|
||||||
essen += [jres["data"][0]["lines"][i]["meals"][0]["name"]]
|
try:
|
||||||
|
jsmeal = jres["data"][0]["lines"][i]["meals"][0]
|
||||||
|
except IndexError:
|
||||||
|
return ["Essen nicht (mehr) verfügbar"]
|
||||||
|
name = jsmeal["name"]
|
||||||
|
vegan = jsmeal["classifiers"].count("VG") == 1
|
||||||
|
schwein = jsmeal["classifiers"].count("S") == 1
|
||||||
|
if vegan:
|
||||||
|
veget = True
|
||||||
|
else:
|
||||||
|
veget = jsmeal["classifiers"].count("VEG") == 1
|
||||||
|
if veget:
|
||||||
|
if name.count("Reibekäse") > 0:
|
||||||
|
vegan = True
|
||||||
|
|
||||||
|
if name != "Tagesdessert":
|
||||||
|
essen += [name]
|
||||||
|
mid = int(time.time()*1000) % 100000
|
||||||
|
neu = Meals(date=day, name=name, id=mid, vegan=vegan, vegetarian=veget, schwein=schwein)
|
||||||
|
db.session.add(neu)
|
||||||
|
db.session.commit()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
essen = ["Tag nicht (mehr) verfügbar"]
|
essen = ["Essen nicht (mehr) verfügbar"]
|
||||||
return essen
|
return essen
|
||||||
@ -3,6 +3,7 @@ from urllib.request import urlretrieve
|
|||||||
import icalendar
|
import icalendar
|
||||||
import json
|
import json
|
||||||
import recurring_ical_events
|
import recurring_ical_events
|
||||||
|
from init import scheduler
|
||||||
|
|
||||||
|
|
||||||
def parseURL(url: str):
|
def parseURL(url: str):
|
||||||
@ -40,11 +41,20 @@ def getNewRapla(url: str):
|
|||||||
|
|
||||||
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", url]})
|
||||||
file.close()
|
file.close()
|
||||||
file = open("calendars/list.json", "w")
|
file = open("calendars/list.json", "w")
|
||||||
json.dump(jsoncal, file, indent=4)
|
json.dump(jsoncal, file, indent=4)
|
||||||
return "rapla"+kurs+".ical"
|
return "rapla" + kurs + ".ical"
|
||||||
|
|
||||||
|
|
||||||
|
def getIcal(kurs: str):
|
||||||
|
file = open("calendars/list.json", "r")
|
||||||
|
jf = json.load(file)
|
||||||
|
try:
|
||||||
|
return jf[kurs]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def getRaplas():
|
def getRaplas():
|
||||||
@ -52,7 +62,18 @@ def getRaplas():
|
|||||||
jsonf = json.load(file)
|
jsonf = json.load(file)
|
||||||
kursl = []
|
kursl = []
|
||||||
filel = []
|
filel = []
|
||||||
|
urll = []
|
||||||
for i in jsonf:
|
for i in jsonf:
|
||||||
kursl += [i]
|
kursl += [i]
|
||||||
filel += [jsonf[i]]
|
filel += [jsonf[i][0]]
|
||||||
return sorted(kursl), sorted(filel)
|
urll += [jsonf[i][1]]
|
||||||
|
return sorted(kursl), sorted(filel), sorted(urll)
|
||||||
|
|
||||||
|
|
||||||
|
@scheduler.task("interval", id="refreshRapla", minutes=30)
|
||||||
|
def refreshRapla():
|
||||||
|
filel = getRaplas()[1]
|
||||||
|
urll = getRaplas()[2]
|
||||||
|
for i in range(len(filel)):
|
||||||
|
urlretrieve(urll[i], "calendars/"+filel[i])
|
||||||
|
print("Update die Kalender...")
|
||||||
|
|||||||
65
init.py
Normal file
65
init.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from flask_login import LoginManager, UserMixin
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from talisman import Talisman
|
||||||
|
from get_mysql import get_mysql
|
||||||
|
import atexit
|
||||||
|
from flask_apscheduler import APScheduler
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Shut down the scheduler when exiting the app
|
||||||
|
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):
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
class Meals(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
date = db.Column(db.Date)
|
||||||
|
name = db.Column(db.String(100))
|
||||||
|
vegetarian = db.Column(db.Boolean)
|
||||||
|
vegan = db.Column(db.Boolean)
|
||||||
|
schwein = db.Column(db.Boolean)
|
||||||
|
|
||||||
|
|
||||||
|
scheduler = APScheduler()
|
||||||
|
app = create()
|
||||||
|
Talisman(app)
|
||||||
|
scheduler.init_app(app)
|
||||||
|
scheduler.start()
|
||||||
|
scheduler.api_enabled = True
|
||||||
@ -1,5 +1,6 @@
|
|||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
Flask
|
Flask
|
||||||
|
Flask_APScheduler
|
||||||
Flask_Login
|
Flask_Login
|
||||||
flask_sqlalchemy
|
flask_sqlalchemy
|
||||||
icalendar
|
icalendar
|
||||||
@ -7,3 +8,4 @@ recurring_ical_events
|
|||||||
Requests
|
Requests
|
||||||
talisman
|
talisman
|
||||||
Werkzeug
|
Werkzeug
|
||||||
|
lxml
|
||||||
|
|||||||
57
templates/plan-anon.html
Normal file
57
templates/plan-anon.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<!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>
|
||||||
|
<form>
|
||||||
|
{% for n in eventdays [e]["mensa"] %}
|
||||||
|
<p class="mensa"> {{ n }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
<a href={{ url_for("login") }}>Einloggen, um alle Features zu nutzen!</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -41,7 +41,7 @@
|
|||||||
<p class="title">{{ i["name"] }}</p>
|
<p class="title">{{ i["name"] }}</p>
|
||||||
<p class="room">{{ i["room"] }}</p>
|
<p class="room">{{ i["room"] }}</p>
|
||||||
<p class="duration">{{ i["dur"] }}</p>
|
<p class="duration">{{ i["dur"] }}</p>
|
||||||
<p class="time"> {{ i["start"] }} - {{ i["end"] }}</p>
|
<p class="time">{{ i["start"] }} - {{ i["end"] }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user