change to use jinja2 and stop saving file and then reading the data

This commit is contained in:
Ugric
2026-01-23 04:51:30 +00:00
parent ecd728fe27
commit 6c17b6c115
6 changed files with 198 additions and 140 deletions

92
main.py
View File

@@ -3,22 +3,23 @@ from datetime import datetime, timezone
import pytz import pytz
import hashlib import hashlib
import os import os
from flask import Flask, request, send_file, render_template from flask import Flask, request, send_file, render_template, Response
import requests import requests
from requests_ntlm import HttpNtlmAuth from requests_ntlm import HttpNtlmAuth
import urllib
# ------------------------------------------------------------ # ------------------------------------------------------------
# Config # Config
# ------------------------------------------------------------ # ------------------------------------------------------------
RESPONSE_FILE = "response.txt"
OLD_CALENDAR = "old_calendar.ics"
NEW_CALENDAR = "calendar.ics"
LOCAL_TZ = pytz.timezone("Europe/London") LOCAL_TZ = pytz.timezone("Europe/London")
URL = "https://webapp.coventry.ac.uk/Timetable-main/Timetable/Current" URL = "https://webapp.coventry.ac.uk/Timetable-main/Timetable/Current"
print(URL.format(urllib.parse.quote_plus("hello/world")))
app = Flask(__name__) app = Flask(__name__)
def parse_events(events_data): def parse_events(events_data):
events_data = events_data.replace("new Date", "") events_data = events_data.replace("new Date", "")
cleaned_data = "" cleaned_data = ""
@@ -52,10 +53,7 @@ def parse_events(events_data):
return parsed_data return parsed_data
def get_events_data_from_file(path): def get_events_data(page_data):
with open(path, "r", encoding="utf-8") as f:
page_data = f.read()
soup = BeautifulSoup(page_data, features="html.parser") soup = BeautifulSoup(page_data, features="html.parser")
for script in soup.head.find_all("script", {"type": "text/javascript"}): for script in soup.head.find_all("script", {"type": "text/javascript"}):
@@ -89,6 +87,7 @@ def create_ics_event(event):
def load_existing_uids(path): def load_existing_uids(path):
uids = set() uids = set()
return uids
if not os.path.exists(path): if not os.path.exists(path):
return uids return uids
@@ -111,6 +110,7 @@ def load_existing_uids(path):
return uids return uids
def fetch_timetable(username, password): def fetch_timetable(username, password):
session = requests.Session() session = requests.Session()
session.auth = HttpNtlmAuth(username, password) session.auth = HttpNtlmAuth(username, password)
@@ -118,16 +118,17 @@ def fetch_timetable(username, password):
r = session.get(URL) r = session.get(URL)
r.raise_for_status() r.raise_for_status()
with open("response.txt", "w", encoding="utf-8") as f: session.close()
f.write(r.text) return r.text
def build_calendar():
events_js = get_events_data_from_file(RESPONSE_FILE) def build_calendar(page_data):
events_js = get_events_data(page_data)
parsed_events = parse_events(events_js) parsed_events = parse_events(events_js)
new_events = [create_ics_event(e) for e in parsed_events if e] new_events = [create_ics_event(e) for e in parsed_events if e]
old_uids = load_existing_uids(OLD_CALENDAR) old_uids = set()
new_uids = {e["uid"] for e in new_events} new_uids = {e["uid"] for e in new_events}
now = ics_time(datetime.now(timezone.utc)) now = ics_time(datetime.now(timezone.utc))
@@ -140,55 +141,58 @@ def build_calendar():
] ]
for ev in new_events: for ev in new_events:
lines.extend([ lines.extend(
"BEGIN:VEVENT", [
f"UID:{ev['uid']}", "BEGIN:VEVENT",
f"DTSTAMP:{now}", f"UID:{ev['uid']}",
f"DTSTART:{ics_time(ev['start'])}", f"DTSTAMP:{now}",
f"DTEND:{ics_time(ev['end'])}", f"DTSTART:{ics_time(ev['start'])}",
f"SUMMARY:{ev['summary']}", f"DTEND:{ics_time(ev['end'])}",
f"DESCRIPTION:{ev['description']}", f"SUMMARY:{ev['summary']}",
"END:VEVENT", f"DESCRIPTION:{ev['description']}",
]) "END:VEVENT",
]
)
for uid in old_uids - new_uids: for uid in old_uids - new_uids:
lines.extend([ lines.extend(
"BEGIN:VEVENT", [
f"UID:{uid}", "BEGIN:VEVENT",
f"DTSTAMP:{now}", f"UID:{uid}",
"STATUS:CANCELLED", f"DTSTAMP:{now}",
"END:VEVENT", "STATUS:CANCELLED",
]) "END:VEVENT",
]
)
lines.append("END:VCALENDAR") lines.append("END:VCALENDAR")
with open(NEW_CALENDAR, "w", encoding="utf-8") as f: return "\n".join(lines)
f.write("\n".join(lines))
# ------------------------------------------------------------ # ------------------------------------------------------------
# Flask # Flask
# ------------------------------------------------------------ # ------------------------------------------------------------
@app.route("/") @app.route("/")
def index(): def index():
return render_template("index.html") return render_template("login.jinja")
@app.route("/login-and-download", methods=["POST"]) @app.route("/login-and-download", methods=["POST"])
def login_and_download(): def login_and_download():
data = request.get_json() data = request.form
username = data["username"] username = data["username"]
password = data["password"] password = data["password"]
username = f'COVENTRY\\{username}'
try: try:
fetch_timetable(username, password) #request data = build_calendar(fetch_timetable(f"COVENTRY\\{username}", password))
build_calendar() return Response(
return send_file( data,
NEW_CALENDAR, mimetype="text/calendar",
as_attachment=True, headers={"Content-Disposition": f'attachment; filename="{username}-timetable.ics"'},
download_name="timetable.ics",
mimetype="text/calendar"
) )
except Exception as e: except Exception as e:
return str(e), 400 return render_template("login.jinja", error=str(e)), 401
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True) app.run(debug=True)

View File

@@ -1,5 +1,24 @@
beautifulsoup4==4.14.3 beautifulsoup4==4.14.3
blinker==1.9.0
bs4==0.0.2 bs4==0.0.2
certifi==2026.1.4
cffi==2.0.0
charset-normalizer==3.4.4
click==8.3.1
cryptography==46.0.3
Flask==3.1.2
gunicorn==24.0.0
idna==3.11
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
packaging==26.0
pycparser==3.0
pyspnego==0.12.0
pytz==2025.2 pytz==2025.2
requests==2.32.5
requests_ntlm==1.3.0
soupsieve==2.8.2 soupsieve==2.8.2
typing_extensions==4.15.0 typing_extensions==4.15.0
urllib3==2.6.3
Werkzeug==3.1.5

94
static/styles.css Normal file
View File

@@ -0,0 +1,94 @@
:root {
--text: #e3f2fc;
--background: #031621;
--primary: #a1c5da;
--secondary: #6a139b;
--accent: #52ace0;
}
body {
font-family: Arial, sans-serif;
color: var(--text);
margin: 0;
background: var(--background);
/* fallback dark color */
}
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 200vw;
height: 200vh;
background: radial-gradient(circle at 50% 0%, var(--accent) 0%, var(--background) 70%);
animation: moveLight 30s ease-in-out infinite alternate;
z-index: -1;
}
@keyframes moveLight {
0% {
transform: translate(-50%, -50%);
}
50% {
transform: translate(-25%, -40%);
}
100% {
transform: translate(0%, -50%);
}
}
.login-box {
position: absolute;
inset: 0px;
width: fit-content;
height: fit-content;
max-width: 100vw;
max-height: 100dvh;
margin: auto;
background: var(--background);
padding: 30px;
border-radius: 8px;
text-align: center;
border: solid 1px var(--accent);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
width: 300px;
}
input {
width: 100%;
padding: 8px;
margin: 8px 0;
background-color: var(--background);
color: var(--text);
border: solid 1px var(--accent);
border-radius: 8px;
}
button {
margin-top: 1rem;
width: 100%;
padding: 10px;
background: var(--accent);
color: var(--background);
font-size: large;
font-weight: bold;
border: none;
cursor: pointer;
}
button:hover {
background: var(--primary);
color: var(--background);
}
.error {
color: red;
margin-top: 10px;
}

21
templates/base.jinja Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/png" sizes="96x96" href="{{ url_for('static', filename='favicon-96x96.png') }}">
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}">
<meta name="apple-mobile-web-app-title" content="Timtabla">
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}">
<meta charset="UTF-8">
<title>Timtabla{% if page_title|trim %} - {{ page_title|trim }}{% endif %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
{% block head %}{% endblock %}
</head>
<body>
</body>
{% block body %}{% endblock %}
</html>

View File

@@ -1,96 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/png" sizes="96x96" href="{{ url_for('static', filename='favicon-96x96.png') }}">
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}">
<meta name="apple-mobile-web-app-title" content="Timtabla">
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}">
<meta charset="UTF-8">
<title>Timtabla</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #03551B, #0b3c5d);
}
.login-box {
background:white;
padding:30px;
border-radius:8px;
box-shadow:0 0 10px rgba(0,0,0,0.1);
width:300px;
}
input {
width:100%;
padding:8px;
margin:8px 0;
}
button {
width:100%;
padding:10px;
background:#0066cc;
color:white;
border:none;
cursor:pointer;
}
button:hover {
background:#004999;
}
.error {
color:red;
margin-top:10px;
}
</style>
</head>
<body>
<div class="login-box">
<h2>Login to Download</h2>
<input type="text" id="username" placeholder="Username" required>
<input type="password" id="password" placeholder="Password" required>
<button onclick="loginAndDownload()">Login & Download</button>
<div class="error" id="errorMsg"></div>
</div>
<script>
async function loginAndDownload() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const errorMsg = document.getElementById("errorMsg");
errorMsg.textContent = "";
try {
const response = await fetch("/login-and-download", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password })
});
if (!response.ok) {
throw new Error("Invalid login or server error");
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "timetable.ics";
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (err) {
errorMsg.textContent = err.message;
}
}
</script>
</body>
</html>

16
templates/login.jinja Normal file
View File

@@ -0,0 +1,16 @@
{%set page_title = "Login"%}
{% extends "base.jinja" %}
{% block body %}
<form class="login-box" method="POST" action="/login-and-download">
<h2>Coventry University Login</h2>
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login & Download</button>
<!-- Optional: server can re-render this with an error message -->
{% if error|trim %}<div class="error">{{error}}</div>{% endif %}
</form>
{% endblock %}