finished backend upgrades to allow multiple courses

This commit is contained in:
2025-10-19 13:50:46 +02:00
parent 02fc60151f
commit 01f5e8ab08
8 changed files with 218 additions and 118 deletions

View File

@@ -3,6 +3,7 @@ from flask_socketio import SocketIO
from .models import db from .models import db
from .utils import init_password_generator, init_serializer from .utils import init_password_generator, init_serializer
from .auth import init_auth from .auth import init_auth
from .seed_db import seed_courses
socketio = SocketIO(async_mode='eventlet', cors_allowed_origins="*") socketio = SocketIO(async_mode='eventlet', cors_allowed_origins="*")
@@ -13,11 +14,13 @@ def create_app():
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
seed_courses()
init_password_generator() init_password_generator()
init_serializer(app) init_serializer(app)
init_auth(app) init_auth(app)
socketio.init_app(app) socketio.init_app(app)
from .routes import main from .routes import main

View File

@@ -11,13 +11,12 @@ def init_auth(app: Flask):
login_manager.init_app(app) login_manager.init_app(app)
login_manager.login_view = "main.admin_login" # type: ignore login_manager.login_view = "main.admin_login" # type: ignore
global users global users
users = {"admin": User("admin", app.config["ADMIN_PASSWORD"])} users = {"admin": User("admin", app.config["ADMIN_PASSWORD"], app.config["DEFAULT_COURSE"])}
class User(flask_login.UserMixin): class User(flask_login.UserMixin):
def __init__(self, id, password): def __init__(self, id, password, active_course_id):
self.id = id self.id = id
self.password = password self.password = password
self.active_course_id = active_course_id
@login_manager.user_loader @login_manager.user_loader
def user_loader(id: str): def user_loader(id: str):
@@ -25,4 +24,3 @@ def user_loader(id: str):
def check_password(user: User, input: str): def check_password(user: User, input: str):
return user.password == input return user.password == input

View File

@@ -5,7 +5,7 @@ from typing import List, Optional
import pytz import pytz
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Boolean, Column, DateTime, String, TypeDecorator, ForeignKey from sqlalchemy import Boolean, Column, DateTime, String, TypeDecorator, ForeignKey, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
db : SQLAlchemy = SQLAlchemy() db : SQLAlchemy = SQLAlchemy()
@@ -61,10 +61,15 @@ class Password(db.Model):
class Deck(db.Model): class Deck(db.Model):
__tablename__ = "deck" __tablename__ = "deck"
__table_args__ = (
UniqueConstraint('course_id', 'deck', name='_course_deck_uc'),
)
id : Mapped[int] = mapped_column(primary_key=True) id : Mapped[int] = mapped_column(primary_key=True)
name : Mapped[str] = mapped_column(String(60), unique=True, nullable=False) name : Mapped[str] = mapped_column(String(60), unique=True, nullable=False)
deck : Mapped[str] = mapped_column(String(60), unique=True, nullable=False) deck : Mapped[str] = mapped_column(String(60), nullable=False)
description : Mapped[str] = mapped_column(String(60)) description : Mapped[str] = mapped_column(String(60))
index_file: Mapped[str] = mapped_column(String(40), nullable=False)
passwords : Mapped[List[Password]] = relationship("Password", secondary=deck_password, back_populates='decks') passwords : Mapped[List[Password]] = relationship("Password", secondary=deck_password, back_populates='decks')
tokens : Mapped[List[Token]] = relationship("Token", secondary=deck_token, back_populates='decks') tokens : Mapped[List[Token]] = relationship("Token", secondary=deck_token, back_populates='decks')
@@ -72,14 +77,15 @@ class Deck(db.Model):
course: Mapped['Course'] = relationship("Course", back_populates='decks') course: Mapped['Course'] = relationship("Course", back_populates='decks')
def __init__(self, name: str, deck: str, course: Course, description: str = "") -> None: def __init__(self, name: str, deck: str, course: Course, description: str = "", index_file: str = "index.html") -> None:
self.name = name self.name = name
self.deck = deck self.deck = deck
self.description = description self.description = description
self.course = course self.course = course
self.index_file = index_file
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Deck(name={self.name}, deck={self.deck}, course={self.course})>" return f"<Deck(name={self.name}, deck={self.deck}, course={self.course}, index={self.index_file})>"
class Token(db.Model): class Token(db.Model):
__tablename__ = "token" __tablename__ = "token"
@@ -101,11 +107,13 @@ class Token(db.Model):
class Course(db.Model): class Course(db.Model):
__tablename__ = "course" __tablename__ = "course"
id : Mapped[int] = mapped_column(primary_key=True) id : Mapped[int] = mapped_column(primary_key=True)
folder: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False) name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False)
decks: Mapped[List['Deck']] = relationship(back_populates='course') decks: Mapped[List['Deck']] = relationship(back_populates='course')
def __init__(self, name: str) -> None: def __init__(self, name: str, folder: str) -> None:
self.name = name self.name = name
self.folder = folder
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Course(id={self.id}, name={self.name})>" return f"<Course(id={self.id}, name={self.name})>"

View File

@@ -9,6 +9,7 @@ from flask_socketio import join_room
import pytz import pytz
from flask import ( from flask import (
Blueprint, Blueprint,
Response,
abort, abort,
current_app, current_app,
flash, flash,
@@ -24,13 +25,14 @@ from . import socketio
from .auth import check_password, user_loader from .auth import check_password, user_loader
from .forms import DeckForm, InfoBannerForm, LoginForm, PasswordForm from .forms import DeckForm, InfoBannerForm, LoginForm, PasswordForm
from .models import Deck, InfoBanner, Password, db from .models import Course, Deck, InfoBanner, Password, db
from .utils import ( from .utils import (
create_token, create_token,
generate_password, generate_password,
get_active_banner, get_active_banner,
get_all_courses, get_all_courses,
get_available_decks, get_available_decks,
get_passwords,
get_token, get_token,
get_unlocked_decks, get_unlocked_decks,
is_deck_unlocked, is_deck_unlocked,
@@ -43,7 +45,7 @@ from .utils import (
main = Blueprint('main', __name__) main = Blueprint('main', __name__)
slidev_deck_pattern = re.compile(r"^\/slides\/[^\/]+\/\d*(?:\?.*)?$") slidev_deck_pattern = re.compile(r"^\/slides\/[^\/]+\/\d*(?:\?.*)?$")
slidev_all_pattern = re.compile(r"^\/slides\/[^\/]+(?:\/presenter)?\/\d+(?:\?.*)?$") slidev_all_pattern = re.compile(r"^\/slides\/([^\/]+)\/([^\/]+)(?:\/presenter)?\/(\d+)(?:\?.*)?$")
@socketio.on("connect") @socketio.on("connect")
def on_connect(): def on_connect():
@@ -100,7 +102,7 @@ def index():
if not token: if not token:
return redirect(url_for("main.set_token")) return redirect(url_for("main.set_token"))
else: else:
return render_template('index.html', active_banner=get_active_banner(), decks=get_unlocked_decks(token), user_name=token.name, active_course_id=token.active_course_id, courses=get_all_courses()) return render_template('index.html', active_banner=get_active_banner(), decks=get_unlocked_decks(token), user_name=token.name, active_course=token.active_course, courses=get_all_courses())
@main.route('/set_token', methods=['POST', 'GET']) @main.route('/set_token', methods=['POST', 'GET'])
def set_token(): def set_token():
@@ -190,6 +192,7 @@ def switch_course(new_course: int):
return redirect(url_for("main.index")) return redirect(url_for("main.index"))
@main.route('/admin/login', methods=['POST', 'GET']) @main.route('/admin/login', methods=['POST', 'GET'])
def admin_login(): def admin_login():
form = LoginForm() form = LoginForm()
@@ -214,27 +217,34 @@ def admin_login():
return render_template("admin_login.html", form=form) return render_template("admin_login.html", form=form)
def process_access_code(access_code: str):
password = db.session.execute(
db.select(Password)
.where(Password.value == access_code)
).scalar_one_or_none()
if password is not None:
if not password.is_expired():
update_token(password.decks)
else:
flash('Password has expired. Please obtain a new one.', 'expired')
else:
flash('Password is not valid. Please check the code and try again.', 'error')
return redirect(url_for('main.index'))
@main.route('/access', methods=['POST']) @main.route('/access', methods=['POST'])
def access(): def access_post():
password = db.session.execute(db.select(Password).where(Password.value == request.form.get('password'))).scalar() access_code = request.form.get('password')
assert isinstance(password, Password|None)
token = get_token() if not access_code:
if password is not None and not password.is_expired(): return redirect(url_for("main.index"))
update_token(password.decks)
return redirect(url_for('main.index')) return process_access_code(access_code)
return render_template('index.html', error="Invalid password.", decks=get_unlocked_decks(token))
# Route for direct link/QR code access (GET)
@main.route('/access/<access_code>', methods=['GET']) @main.route('/access/<access_code>', methods=['GET'])
def access_link(access_code: str): def access_get(access_code: str):
password = db.session.execute(db.select(Password).where(Password.value == access_code)).scalar() return process_access_code(access_code)
assert isinstance(password, Password|None)
token = get_token()
if password is not None and not password.is_expired():
update_token(password.decks)
return redirect(url_for('main.index'))
return render_template('index.html', error="Invalid password.", decks=get_unlocked_decks(token))
@main.route('/confused', methods=['POST']) @main.route('/confused', methods=['POST'])
@@ -248,25 +258,29 @@ def confused():
) )
return ("", 204) return ("", 204)
@main.route('/slides/<deck>/') @main.route('/slides/<course_folder>/<deck>/')
@main.route('/slides/<deck>/<path:path>') @main.route('/slides/<course_folder>/<deck>/<path:path>')
def serve_deck(deck, path='index.html'): def serve_deck(deck, course_folder, path='index.html'):
if not db.session.execute(db.select(Deck).where(Deck.deck==deck)).scalar(): course = db.session.execute(db.select(Course).where(Course.folder==course_folder)).scalar()
if not course:
abort(404) abort(404)
assert isinstance(course, Course)
deck = db.session.execute(db.select(Deck).where(Deck.deck==deck and Deck.course_id == course.id)).scalar()
if not deck:
abort(404)
assert isinstance(deck, Deck)
if not is_deck_unlocked(deck): if not is_deck_unlocked(deck.deck):
return redirect(url_for('main.index')) return redirect(url_for('main.index'))
slides_dir = current_app.config['SLIDES_DIR'] slides_dir = os.path.join(current_app.config['SLIDES_DIR'], course.folder)
deck_path = os.path.join(slides_dir, deck) deck_path = os.path.join(slides_dir, deck.deck)
if not os.path.exists(deck_path): if not os.path.exists(deck_path):
return f"Deck '{deck}' not found.", 404 return f"Deck '{deck}' not found.", 404
if slidev_all_pattern.match(request.path): if slidev_all_pattern.match(request.path):
return send_from_directory(deck_path, 'index.html') return send_from_directory(deck_path, deck.index_file)
if deck in ["tut-08","tut-11"] and path == 'index.html':
return send_from_directory(deck_path, 'slides.pdf')
requested_file = os.path.join(deck_path, path) requested_file = os.path.join(deck_path, path)
if os.path.isfile(requested_file): if os.path.isfile(requested_file):
@@ -277,13 +291,16 @@ def serve_deck(deck, path='index.html'):
@main.route('/admin', methods=['GET', 'POST']) @main.route('/admin', methods=['GET', 'POST'])
@flask_login.login_required @flask_login.login_required
def admin(): def admin():
active_course_id = flask_login.current_user.active_course_id
active_course = get_course(active_course_id)
if active_course is None:
raise Exception
deck_form = DeckForm() deck_form = DeckForm()
password_form = PasswordForm() password_form = PasswordForm()
if request.method == 'GET': if request.method == 'GET':
password_form.value.data = generate_password() password_form.value.data = generate_password()
deck_form.deck.choices = get_available_decks(current_app) deck_form.deck.choices = get_available_decks(current_app, active_course)
decks = db.session.scalars(db.select(Deck).order_by(Deck.name)).all() decks = active_course.decks
passwords = db.session.scalars(db.select(Password)).all()
password_form.decks.choices = [(deck.id, deck.name) for deck in decks] password_form.decks.choices = [(deck.id, deck.name) for deck in decks]
@@ -292,6 +309,7 @@ def admin():
deck = Deck( deck = Deck(
name=cast(str,deck_form.name.data), name=cast(str,deck_form.name.data),
deck=deck_form.deck.data, deck=deck_form.deck.data,
course=active_course,
description=cast(str,deck_form.description.data), description=cast(str,deck_form.description.data),
) )
db.session.add(deck) db.session.add(deck)
@@ -320,7 +338,15 @@ def admin():
print(f"Error in {getattr(password_form, field).label.text}: {error}", flush=True) print(f"Error in {getattr(password_form, field).label.text}: {error}", flush=True)
return redirect(url_for('main.admin')) return redirect(url_for('main.admin'))
return render_template("admin.html", deck_form=deck_form, password_form=password_form, decks=decks, passwords=passwords) return render_template("admin.html", deck_form=deck_form, password_form=password_form, decks=decks, passwords=get_passwords(active_course), courses=get_all_courses(), active_course_id=active_course_id)
@main.route('/admin/switch_course/<new_course>', methods=['POST'])
@flask_login.login_required
def switch_course_admin(new_course: int):
user = flask_login.current_user
if get_course(new_course):
user.active_course_id = new_course
return redirect(url_for("main.admin"))
@main.route('/admin/banner', methods=['POST', 'GET']) @main.route('/admin/banner', methods=['POST', 'GET'])
@flask_login.login_required @flask_login.login_required
@@ -340,7 +366,6 @@ def delete_deck(deck_id: int):
deck = Deck.query.get_or_404(deck_id) deck = Deck.query.get_or_404(deck_id)
db.session.delete(deck) db.session.delete(deck)
db.session.commit() db.session.commit()
flash(f'Deck "{deck.name}" deleted.', 'success')
return redirect(url_for('main.admin')) return redirect(url_for('main.admin'))
@main.route('/admin/passwords/<int:password_id>/delete', methods=['POST']) @main.route('/admin/passwords/<int:password_id>/delete', methods=['POST'])
@@ -349,7 +374,6 @@ def delete_password(password_id: int):
password = Password.query.get_or_404(password_id) password = Password.query.get_or_404(password_id)
db.session.delete(password) db.session.delete(password)
db.session.commit() db.session.commit()
flash(f'Password "{password.value}" deleted.', 'success')
return redirect(url_for('main.admin')) return redirect(url_for('main.admin'))
@main.route('/admin/banner/show/<int:banner_id>', methods=['POST']) @main.route('/admin/banner/show/<int:banner_id>', methods=['POST'])

26
app/seed_db.py Normal file
View File

@@ -0,0 +1,26 @@
# Assuming db and models (Course) are initialized/imported
from .models import db, Course
def seed_courses():
course_data = [
{'id': 1, 'folder': 'grnvs25', 'name': 'GRNVS Tutorium SS 25'},
{'id': 2, 'folder': 'itsec2526', 'name': 'IT-Sec Tutorium WS 25/26'},
]
for data in course_data:
# Check if the course already exists by ID
existing_course = db.session.get(Course, data['id'])
if existing_course is None:
# Create a new Course object if it doesn't exist
new_course = Course(name=data['name'], folder=data['folder'])
# Since you're specifying the ID, you need to set it manually
# if it's not an autoincrement sequence (SQLite handles this fine)
new_course.id = data['id']
db.session.add(new_course)
# If it exists, you might want to update the name:
elif existing_course.name != data['name']:
existing_course.name = data['name']
db.session.commit()

View File

@@ -34,26 +34,53 @@
</head> </head>
<body class="bg-background text-text min-h-screen font-sans"> <body class="bg-background text-text min-h-screen font-sans">
<nav class="bg-white border-gray-200 dark:bg-gray-900"> <nav class="bg-white border-gray-200 dark:bg-gray-900">
<div class="max-w-screen-xl flex flex-wrap items-center x-auto p-4"> <div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<a href="{{url_for('main.index')}}" class="flex items-center space-x-3 rtl:space-x-reverse"> <div class="flex items-center space-x-6 rtl:space-x-reverse">
<a href="{{ url_for('main.index') }}" class="flex items-center">
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Tutortool</span> <span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Tutortool</span>
</a> </a>
<div class="relative group">
<button class="flex items-center text-gray-900 dark:text-white hover:text-blue-700 dark:hover:text-blue-500 font-medium py-2 px-3 rounded-lg bg-transparent">
{% for course in courses %}
{% if course.id == active_course_id %}
{{ course.name }}
{% endif %}
{% endfor %}
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg>
</button>
<div class="absolute z-10 hidden group-hover:block w-44 bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
{% for course in courses %}
<li>
<form method="POST" action="/admin/switch_course/{{ course.id }}">
<button type="submit">{{ course.name }}</button>
</form>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="w-auto" id="navbar-default"> <div class="w-auto" id="navbar-default">
<ul class="font-medium p-0 py-2 border border-gray-100 rounded-lg bg-gray-50 flex flex-row space-x-8 rtl:space-x-reverse mt-0 border-0 bg-white dark:bg-gray-800 dark:bg-gray-900 dark:border-gray-700 justify-between"> <ul class="flex flex-row space-x-8 rtl:space-x-reverse font-medium p-0 border-0 bg-white dark:bg-gray-900">
<li> <li>
<a href="{{url_for('main.admin')}}" class="rounded-sm bg-transparent text-blue-700 p-0 dark:text-blue-500" aria-current="page">Deck Admin</a> <a href="{{ url_for('main.admin') }}" class="rounded-sm text-blue-700 dark:text-blue-500 font-semibold" aria-current="page">Deck Admin</a>
</li> </li>
<li> <li>
<a href="{{url_for('main.banner')}}" class="text-grey-900 dark:hover:text-blue-500 hover:text-blue-700 rounded-sm bg-transparent dark:text-white">Banner</a> <a href="{{ url_for('main.banner') }}" class="text-gray-900 dark:text-white hover:text-blue-700 dark:hover:text-blue-500">Banner</a>
</li> </li>
<li> <li>
<a href="{{url_for('main.confused_notifications')}}" class="text-grey-900 dark:hover:text-blue-500 hover:text-blue-700 rounded-sm bg-transparent p-0 dark:text-white">Confused</a> <a href="{{ url_for('main.confused_notifications') }}" class="text-gray-900 dark:text-white hover:text-blue-700 dark:hover:text-blue-500">Confused</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</nav> </nav>
<div class="container mx-auto px-6 py-10 space-y-12"> <div class="container mx-auto px-6 py-10 space-y-12">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-10">

View File

@@ -32,6 +32,8 @@
<!-- in base.html, in <head> or just before </body> --> <!-- in base.html, in <head> or just before </body> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.1/socket.io.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.1/socket.io.min.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head> </head>
@@ -41,66 +43,35 @@
<!-- Header --> <!-- Header -->
<header class="bg-surface border-b border-border shadow-sm"> <header class="bg-surface border-b border-border shadow-sm">
<div <div class="w-full px-6 py-4 flex items-center justify-between" >
class="w-full px-6 py-4
flex items-center justify-between"
>
<div class="relative z-20">
<button id="menu-toggle" class="text-white hover:text-gray-300 focus:outline-none p-2 rounded-md">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<div class="hamburger-menu"> <div class="flex items-center space-x-3">
<button class="menu-toggle" aria-controls="course-selector">Menu</button> <span class="text-gray-700 dark:text-gray-200 text-base font-medium whitespace-nowrap">
User: {{ user_name }}
</span>
<select id="course-selector" name="course_selection" onchange="this.form.submit()"> <form method="POST" action="/logout">
<option value="" disabled selected>Select a Course</option> <button
type="submit"
{% for course in courses %} class="text-sm text-white bg-red-600 px-3 py-1 rounded-md hover:bg-red-700 transition"
<option value="{{ course.id }}"> >
{{ course.display_name }} Logout
</option> </button>
{% endfor %} </form>
</select>
</div>
<div class="flex items-center space-x-1 relative z-10">
<span class="text-white text-base font-medium whitespace-nowrap py-1">
User: {{ user_name }}
</span>
<form method="POST" action="/logout"> </div>
<button <div class="relative z-10">
type="submit" <a href="/admin">
class="text-sm text-white bg-red-600 px-3 py-1 rounded-md hover:bg-red-700 transition" <button class="bg-primary hover:bg-blue-700 text-white px-3 py-1 rounded-md transition">
> Admin
Logout </button>
</button> </a>
</form>
<a href="/admin" class="ml-4">
<button class="bg-primary hover:bg-blue-700 text-white px-3 py-1 rounded-md transition">
Admin
</button>
</a>
</div> </div>
</div> </div>
</header> </header> <!-- Main Content -->
<script>
document.getElementById('menu-toggle').addEventListener('click', function() {
document.getElementById('course-menu').classList.toggle('hidden');
});
</script> <!-- Main Content -->
<main class="max-w-5xl mx-auto px-6 py-10 space-y-12"> <main class="max-w-5xl mx-auto px-6 py-10 space-y-12">
<h3 class="text-3xl font-bold text-center mb-8">{{course_display_name}}</h3>
{% if active_banner %} {% if active_banner %}
<section class="flex justify-center"> <section class="flex justify-center">
<div class="p-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-gray-800 dark:text-yellow-300" role="alert"> <div class="p-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-gray-800 dark:text-yellow-300" role="alert">
@@ -109,6 +80,33 @@
</section> </section>
{% endif %} {% endif %}
<div class="flex justify-center">
<div class="relative group">
<button class="flex items-center text-gray-900 dark:text-white hover:text-blue-700 dark:hover:text-blue-500 font-medium py-1 px-3 rounded-lg bg-transparent text-2xl">
{% for course in courses %}
{% if course.id == active_course.id %}
{{ course.name }}
{% endif %}
{% endfor %}
<svg class="w-2.5 h-2.5 ms-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg>
</button>
<div class="absolute hidden group-hover:block bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 z-50">
<ul class="py-2 px-4 text-sm text-gray-700 dark:text-gray-200">
{% for course in courses %}
<li>
<form method="POST" action="/switch_course/{{ course.id }}">
<button type="submit"> {{ course.name }}</button>
</form>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<!-- Password Form --> <!-- Password Form -->
<section class="flex justify-center"> <section class="flex justify-center">
<div class="w-full max-w-md"> <div class="w-full max-w-md">
@@ -130,9 +128,21 @@
Unlock Unlock
</button> </button>
</form> </form>
{% if error %} {% with messages = get_flashed_messages(with_categories=true) %}
<p class="text-red-500 text-center mt-3">{{ error }}</p> {% if messages %}
{% endif %} <div class="flashed-messages">
{% for category, message in messages %}
{% if category == 'error' %}
<p class="text-red-500 text-center mt-3">{{ message }}</p>
{% elif category == 'expired' %}
<p class="text-orange-500 text-center mt-3">{{ message }}</p>
{% else %}
<p class="text-gray-500 text-center mt-3">{{ message }}</p>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div> </div>
</div> </div>
</section> </section>
@@ -142,7 +152,7 @@
<section> <section>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{% for deck in decks %} {% for deck in decks %}
<a href="/slides/{{ deck.deck }}/" class="block bg-surface border border-border rounded-xl p-5 shadow-sm hover:border-primary hover:bg-background transition"> <a href="/slides/{{deck.course.folder}}/{{ deck.deck }}/{{ deck.index_file}}" class="block bg-surface border border-border rounded-xl p-5 shadow-sm hover:border-primary hover:bg-background transition">
<h3 class="text-lg font-semibold mb-1">{{ deck.name }}</h3> <h3 class="text-lg font-semibold mb-1">{{ deck.name }}</h3>
<p class="text-muted text-sm">{{ deck.description }}</p> <p class="text-muted text-sm">{{ deck.description }}</p>
</a> </a>

View File

@@ -19,8 +19,8 @@ def init_serializer(app: Flask):
def get_active_banner() -> InfoBanner | None: def get_active_banner() -> InfoBanner | None:
return db.session.execute(db.select(InfoBanner).where(InfoBanner.is_active)).scalar_one_or_none() return db.session.execute(db.select(InfoBanner).where(InfoBanner.is_active)).scalar_one_or_none()
def get_available_decks(app: Flask): def get_available_decks(app: Flask, course: Course):
slides_dir = app.config['SLIDES_DIR'] slides_dir = os.path.join(app.config['SLIDES_DIR'], course.folder)
deck_names = [name for name in os.listdir(slides_dir) if os.path.isdir(os.path.join(slides_dir, name))] deck_names = [name for name in os.listdir(slides_dir) if os.path.isdir(os.path.join(slides_dir, name))]
deck_names.sort() deck_names.sort()
return deck_names return deck_names
@@ -31,6 +31,9 @@ def generate_username() -> str:
def generate_password() -> str: def generate_password() -> str:
return cast(str, xkcd_password.generate_xkcdpassword(wordlist=words, numwords=2, delimiter="-")) return cast(str, xkcd_password.generate_xkcdpassword(wordlist=words, numwords=2, delimiter="-"))
def get_passwords(course: Course):
return [password for deck in course.decks for password in deck.passwords]
def generate_token() -> Token: def generate_token() -> Token:
max_tries = 100 max_tries = 100
for _ in range(max_tries): for _ in range(max_tries):
@@ -68,12 +71,13 @@ def retrieve_token(username : str):
def get_unlocked_decks(token: (Token|None)): def get_unlocked_decks(token: (Token|None)):
if token is None: if token is None:
return [] return []
decks = token.decks decks = [deck for deck in token.decks if deck.course_id == token.active_course_id]
decks.sort(key=lambda deck: deck.deck) decks.sort(key=lambda deck: deck.deck)
return decks return decks
def is_deck_unlocked(deck: str): def is_deck_unlocked(deck: str):
token = get_token() token = get_token()
print(token)
if token: if token:
return token.is_unlocked(deck) return token.is_unlocked(deck)
else: else: