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,13 +27,13 @@
</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 #}
<div class="flex items-center gap-3">
{# 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>
@@ -46,7 +46,37 @@
</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">
</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>
</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,