made things prettier

This commit is contained in:
2026-02-22 01:16:20 +01:00
parent 076293f80d
commit 7564da1148
10 changed files with 388 additions and 626 deletions

View File

@@ -1,5 +1,7 @@
import os
import logging
import markdown
from markupsafe import Markup
from logging.handlers import WatchedFileHandler
from flask import Flask
from flask_socketio import SocketIO
@@ -49,4 +51,10 @@ def create_app(config_class=Config):
app.register_blueprint(slides_bp)
app.register_blueprint(admin_bp)
@app.template_filter('md')
def render_markdown(text):
if not text:
return ''
return Markup(markdown.markdown(text, extensions=['nl2br']))
return app

744
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.21",
"flowbite": "^3.1.2",
"postcss": "^8.5.6",

View File

@@ -8,82 +8,178 @@ bidict==0.23.1
# via python-socketio
blinker==1.9.0
# via flask
brotli==1.2.0
# via geventhttpclient
cachetools==6.2.6 ; python_full_version < '3.10'
# via tutor-tool
cachetools==7.0.1 ; python_full_version >= '3.10'
# via tutor-tool
certifi==2026.1.4
# via
# geventhttpclient
# requests
cffi==2.0.0 ; (implementation_name == 'pypy' and platform_python_implementation != 'CPython') or (implementation_name == 'pypy' and sys_platform != 'win32') or (platform_python_implementation == 'CPython' and sys_platform == 'win32')
# via
# gevent
# pyzmq
charset-normalizer==3.4.4
# via requests
click==8.1.8 ; python_full_version < '3.10'
# via flask
click==8.2.1 ; python_full_version >= '3.10'
# via flask
colorama==0.4.6 ; sys_platform == 'win32'
# via click
# via
# click
# pytest
configargparse==1.7.1
# via locust
dnspython==2.7.0 ; python_full_version < '3.10'
# via eventlet
dnspython==2.8.0 ; python_full_version >= '3.10'
# via eventlet
eventlet==0.40.3
# via tutor-tool
exceptiongroup==1.3.1 ; python_full_version == '3.10.*'
# via pytest
flask==3.1.2
# via
# flask-bcrypt
# flask-cors
# flask-login
# flask-socketio
# flask-sqlalchemy
# flask-wtf
# locust
# tutor-tool
flask-bcrypt==1.0.1
# via tutor-tool
flask-cors==6.0.2
# via locust
flask-login==0.6.3
# via tutor-tool
# via
# locust
# tutor-tool
flask-socketio==5.5.1
# via tutor-tool
flask-sqlalchemy==3.1.1
# via tutor-tool
flask-wtf==1.2.2
# via tutor-tool
gevent==25.9.1
# via
# geventhttpclient
# locust
geventhttpclient==2.3.7
# via locust
greenlet==3.2.4
# via
# eventlet
# gevent
# sqlalchemy
gunicorn==23.0.0
# via tutor-tool
h11==0.16.0
# via wsproto
idna==3.11
# via requests
importlib-metadata==8.7.0 ; python_full_version < '3.10'
# via flask
# via
# flask
# markdown
iniconfig==2.3.0 ; python_full_version >= '3.10'
# via pytest
itsdangerous==2.2.0
# via
# flask
# flask-wtf
jinja2==3.1.6
# via flask
locust==2.34.0 ; python_full_version < '3.10'
# via tutor-tool
locust==2.43.3 ; python_full_version >= '3.10'
# via tutor-tool
markdown==3.9 ; python_full_version < '3.10'
# via tutor-tool
markdown==3.10.2 ; python_full_version >= '3.10'
# via tutor-tool
markupsafe==3.0.2
# via
# flask
# jinja2
# werkzeug
# wtforms
msgpack==1.1.2
# via locust
packaging==25.0
# via gunicorn
# via
# gunicorn
# pytest
pluggy==1.6.0 ; python_full_version >= '3.10'
# via pytest
psutil==7.2.2
# via locust
psycopg2-binary==2.9.11
# via tutor-tool
pycparser==2.23 ; (python_full_version < '3.10' and implementation_name == 'pypy' and platform_python_implementation != 'CPython') or (python_full_version < '3.10' and implementation_name != 'PyPy' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (python_full_version < '3.10' and implementation_name == 'pypy' and sys_platform != 'win32')
# via cffi
pycparser==3.0 ; (python_full_version >= '3.10' and implementation_name == 'pypy' and platform_python_implementation != 'CPython') or (python_full_version >= '3.10' and implementation_name != 'PyPy' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (python_full_version >= '3.10' and implementation_name == 'pypy' and sys_platform != 'win32')
# via cffi
pygments==2.19.2 ; python_full_version >= '3.10'
# via pytest
pytest==9.0.2 ; python_full_version >= '3.10'
# via locust
python-engineio==4.12.2
# via python-socketio
# via
# locust
# python-socketio
python-socketio==5.13.0
# via flask-socketio
# via
# flask-socketio
# locust
pytz==2025.2
# via tutor-tool
pywin32==311 ; sys_platform == 'win32'
# via locust
pyzmq==27.1.0
# via locust
redis==7.0.1 ; python_full_version < '3.10'
# via tutor-tool
redis==7.2.0 ; python_full_version >= '3.10'
# via tutor-tool
requests==2.32.5
# via
# locust
# python-socketio
setuptools==82.0.0 ; python_full_version < '3.10'
# via
# locust
# zope-event
simple-websocket==1.1.0
# via python-engineio
sqlalchemy==2.0.43
# via flask-sqlalchemy
tomli==2.4.0 ; python_full_version < '3.11'
# via
# locust
# pytest
typing-extensions==4.15.0
# via sqlalchemy
# via
# exceptiongroup
# locust
# sqlalchemy
urllib3==2.6.3
# via
# geventhttpclient
# requests
websocket-client==1.9.0 ; python_full_version >= '3.10'
# via python-socketio
werkzeug==3.1.3
# via
# flask
# flask-cors
# flask-login
# locust
wsproto==1.2.0
# via simple-websocket
wtforms==3.2.1
@@ -92,3 +188,11 @@ xkcdpass==1.20.0
# via tutor-tool
zipp==3.23.0 ; python_full_version < '3.10'
# via importlib-metadata
zope-event==6.0 ; python_full_version < '3.10'
# via gevent
zope-event==6.1 ; python_full_version >= '3.10'
# via gevent
zope-interface==8.0.1 ; python_full_version < '3.10'
# via gevent
zope-interface==8.2 ; python_full_version >= '3.10'
# via gevent

View File

@@ -15,6 +15,7 @@ module.exports = {
},
},
plugins: [
require('flowbite/plugin')
require('flowbite/plugin'),
require('@tailwindcss/typography')
],
}

View File

@@ -11,14 +11,14 @@
{% block body %}
{# ── Header ────────────────────────────────────────────────────── #}
<header class="bg-surface border-b border-border shadow-sm">
<div class="w-full px-6 py-3 flex items-center justify-between gap-4">
<header class="w-full px-4 pt-4 pb-2 md:px-6 md:py-3">
{# Left: user + logout #}
<div class="flex items-center gap-2 min-w-0">
<span class="text-gray-700 dark:text-gray-200 text-sm font-medium truncate">
{{ user_name }}
</span>
{# ── Desktop: single row ───────────────────────────── #}
<div class="hidden md:grid md:grid-cols-3 items-center gap-4">
{# Col 1: user + logout (left-aligned) #}
<div class="flex items-center gap-2">
<span class="text-gray-700 dark:text-gray-200 text-sm font-medium">{{ user_name }}</span>
<form method="POST" action="/logout">
<button type="submit"
class="text-xs text-white bg-red-600 px-2.5 py-1 rounded-md hover:bg-red-700 transition">
@@ -27,12 +27,40 @@
</form>
</div>
{# Center: course switcher #}
{# Col 2: course switcher (perfectly centered) #}
<div class="flex justify-center">
{{ course_switcher(courses, active_course_name, '/switch_course') }}
</div>
{# Right: social links + admin #}
{# Col 3: social + admin (right-aligned) #}
<div class="flex items-center justify-end gap-3">
<a href="https://zulip.in.tum.de/#narrow/channel/3234-ITSec-Tutorium-Simon-Bu.C3.9Fmann" target="_blank">
<img src="{{ url_for('static', filename='zulip_icon.svg') }}" class="h-7 w-7">
</a>
<a href="https://discordapp.com/users/306086176585154560" target="_blank">
<img src="{{ url_for('static', filename='discord_icon.svg') }}" class="h-7 w-7">
</a>
<a href="{{ url_for('admin.admin') }}">
<button class="bg-primary hover:bg-blue-700 text-white text-sm px-3 py-1 rounded-md transition">
Admin
</button>
</a>
</div>
</div>
{# ── Mobile: single row only ──────────────────────── #}
<div class="flex flex-col gap-3 md:hidden">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-gray-700 dark:text-gray-200 text-sm font-medium">{{ user_name }}</span>
<form method="POST" action="/logout">
<button type="submit"
class="text-xs text-white bg-red-600 px-2.5 py-1 rounded-md hover:bg-red-700 transition">
Logout
</button>
</form>
</div>
<div class="flex items-center gap-3">
<a href="https://zulip.in.tum.de/#narrow/channel/3234-ITSec-Tutorium-Simon-Bu.C3.9Fmann" target="_blank">
<img src="{{ url_for('static', filename='zulip_icon.svg') }}" class="h-7 w-7">
@@ -47,6 +75,8 @@
</a>
</div>
</div>
</div>
</header>
{# ── Main Content ──────────────────────────────────────────────── #}
@@ -60,6 +90,9 @@
</section>
{% endif %}
<div class="flex justify-center md:hidden">
{{ course_switcher(courses, active_course_name, '/switch_course') }}
</div>
{# ── Password Form ─────────────────────────────────────────── #}
<section class="flex justify-center">
@@ -110,7 +143,9 @@
<a href="/slides/{{ deck.course.folder }}/{{ deck.deck }}/{{ deck.index_file }}"
class="flex-grow block hover:text-primary transition">
<h3 class="text-lg font-semibold mb-1">{{ deck.name }}</h3>
<p class="text-muted text-sm">{{ deck.description }}</p>
<div class="prose prose-sm dark:prose-invert prose-p:text-muted prose-li:text-muted">
{{ deck.description | md }}
</div>
</a>
{% if deck.export_file %}

View File

@@ -81,8 +81,11 @@
<button
type="button"
onclick="toggleCourseSwitcher()"
class="flex items-center gap-2 px-4 py-1.5 rounded-lg border border-border bg-surface
text-sm font-medium text-text hover:border-primary hover:text-primary transition whitespace-nowrap">
class="flex items-center gap-2 px-4 py-1.5 rounded-full
bg-surface shadow-md ring-1 ring-border
text-sm font-medium text-text
hover:ring-primary hover:text-primary
transition whitespace-nowrap">
<span id="course-switcher-label">{{ active_course_name }}</span>
<svg class="w-3.5 h-3.5 text-muted transition-transform duration-200" id="course-switcher-chevron"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">

View File

@@ -1,11 +1,12 @@
import os
import logging
from cachetools import TTLCache, cached
from eventlet.semaphore import Semaphore
from flask import request, session, redirect, url_for, current_app, abort, make_response
from typing import cast
from itsdangerous import BadSignature, URLSafeSerializer
from xkcdpass import xkcd_password
from functools import wraps, lru_cache
from functools import wraps
from urllib.parse import urlparse, urljoin
from .models import Deck, InfoBanner, Token, db, Course
@@ -13,7 +14,8 @@ from .models import Deck, InfoBanner, Token, db, Course
# Create a module-level logger per Flask/Python standards
logger = logging.getLogger(__name__)
_deck_cache = TTLCache(maxsize=1000, ttl=10)
_deck_cache_lock = Semaphore()
def init_password_generator():
@@ -190,12 +192,14 @@ def token_required(f):
return decorated_function
# Cache the last 1000 lookups per worker.
@lru_cache(maxsize=1000)
@cached(_deck_cache, lock=_deck_cache_lock)
def get_cached_deck_info(token_id, course_folder, deck_slug):
"""
Returns a dictionary of primitive types for route authorization and routing.
Never return raw SQLAlchemy models from an lru_cache!
Never return raw SQLAlchemy models from a cache!
"""
# 1. Fetch Token
token = db.session.get(Token, token_id)
@@ -221,7 +225,6 @@ def get_cached_deck_info(token_id, course_folder, deck_slug):
# 4. Check Authorization
is_unlocked = deck_slug in [d.deck for d in token.decks]
# Return only primitive data (bools, strings) so the cache is thread-safe!
return {
"auth": is_unlocked,
"course_exists": True,

View File

@@ -5,6 +5,7 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"cachetools>=6.2.6",
"eventlet>=0.40.0",
"flask>=3.1.1",
"flask-bcrypt>=1.0.1",
@@ -14,6 +15,7 @@ dependencies = [
"flask-wtf>=1.2.2",
"gunicorn>=23.0.0",
"locust>=2.34.0",
"markdown>=3.9",
"psycopg2-binary>=2.9.11",
"pytz>=2025.2",
"redis>=7.0.1",

57
uv.lock generated
View File

@@ -171,6 +171,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/df/e7c780e463ee7bd7951770692bbea5a605f56b9809ec7f6ce751d7b2ee88/brotli-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:1ce223652fd4ed3eb2b7f78fbea31c52314baecfac68db44037bb4167062a937", size = 369008, upload-time = "2025-11-05T18:39:41.515Z" },
]
[[package]]
name = "cachetools"
version = "6.2.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/39/91/d9ae9a66b01102a18cd16db0cf4cd54187ffe10f0865cc80071a4104fbb3/cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6", size = 32363, upload-time = "2026-01-27T20:32:59.956Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/45/f458fa2c388e79dd9d8b9b0c99f1d31b568f27388f2fdba7bb66bbc0c6ed/cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda", size = 11668, upload-time = "2026-01-27T20:32:58.527Z" },
]
[[package]]
name = "cachetools"
version = "7.0.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" },
]
[[package]]
name = "certifi"
version = "2026.1.4"
@@ -930,6 +954,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/d2/dc5379876d3a481720803653ea4d219f0c26f2d2b37c9243baaa16d0bc79/locust-2.43.3-py3-none-any.whl", hash = "sha256:e032c119b54a9d984cb74a936ee83cfd7d68b3c76c8f308af63d04f11396b553", size = 1463473, upload-time = "2026-02-12T09:55:31.727Z" },
]
[[package]]
name = "markdown"
version = "3.9"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.10'",
]
dependencies = [
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" },
]
[[package]]
name = "markdown"
version = "3.10.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
@@ -1569,6 +1620,8 @@ name = "tutor-tool"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "cachetools", version = "6.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "cachetools", version = "7.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "eventlet" },
{ name = "flask" },
{ name = "flask-bcrypt" },
@@ -1579,6 +1632,8 @@ dependencies = [
{ name = "gunicorn" },
{ name = "locust", version = "2.34.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "locust", version = "2.43.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "psycopg2-binary" },
{ name = "pytz" },
{ name = "redis", version = "7.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
@@ -1588,6 +1643,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "cachetools", specifier = ">=6.2.6" },
{ name = "eventlet", specifier = ">=0.40.0" },
{ name = "flask", specifier = ">=3.1.1" },
{ name = "flask-bcrypt", specifier = ">=1.0.1" },
@@ -1597,6 +1653,7 @@ requires-dist = [
{ name = "flask-wtf", specifier = ">=1.2.2" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "locust", specifier = ">=2.34.0" },
{ name = "markdown", specifier = ">=3.9" },
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
{ name = "pytz", specifier = ">=2025.2" },
{ name = "redis", specifier = ">=7.0.1" },