made things prettier
This commit is contained in:
@@ -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
744
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,6 +15,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('flowbite/plugin')
|
||||
require('flowbite/plugin'),
|
||||
require('@tailwindcss/typography')
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
17
app/utils.py
17
app/utils.py
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user