finished backend upgrades to allow multiple courses
This commit is contained in:
@@ -3,6 +3,7 @@ from flask_socketio import SocketIO
|
||||
from .models import db
|
||||
from .utils import init_password_generator, init_serializer
|
||||
from .auth import init_auth
|
||||
from .seed_db import seed_courses
|
||||
|
||||
socketio = SocketIO(async_mode='eventlet', cors_allowed_origins="*")
|
||||
|
||||
@@ -13,11 +14,13 @@ def create_app():
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
seed_courses()
|
||||
|
||||
init_password_generator()
|
||||
init_serializer(app)
|
||||
init_auth(app)
|
||||
|
||||
|
||||
socketio.init_app(app)
|
||||
|
||||
from .routes import main
|
||||
|
||||
@@ -11,13 +11,12 @@ def init_auth(app: Flask):
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = "main.admin_login" # type: ignore
|
||||
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):
|
||||
def __init__(self, id, password):
|
||||
def __init__(self, id, password, active_course_id):
|
||||
self.id = id
|
||||
self.password = password
|
||||
self.active_course_id = active_course_id
|
||||
|
||||
@login_manager.user_loader
|
||||
def user_loader(id: str):
|
||||
@@ -25,4 +24,3 @@ def user_loader(id: str):
|
||||
|
||||
def check_password(user: User, input: str):
|
||||
return user.password == input
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import List, Optional
|
||||
|
||||
import pytz
|
||||
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
|
||||
db : SQLAlchemy = SQLAlchemy()
|
||||
|
||||
@@ -61,10 +61,15 @@ class Password(db.Model):
|
||||
class Deck(db.Model):
|
||||
__tablename__ = "deck"
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('course_id', 'deck', name='_course_deck_uc'),
|
||||
)
|
||||
|
||||
id : Mapped[int] = mapped_column(primary_key=True)
|
||||
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))
|
||||
index_file: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
|
||||
passwords : Mapped[List[Password]] = relationship("Password", secondary=deck_password, 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')
|
||||
|
||||
|
||||
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.deck = deck
|
||||
self.description = description
|
||||
self.course = course
|
||||
self.index_file = index_file
|
||||
|
||||
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):
|
||||
__tablename__ = "token"
|
||||
@@ -101,11 +107,13 @@ class Token(db.Model):
|
||||
class Course(db.Model):
|
||||
__tablename__ = "course"
|
||||
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)
|
||||
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.folder = folder
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Course(id={self.id}, name={self.name})>"
|
||||
|
||||
100
app/routes.py
100
app/routes.py
@@ -9,6 +9,7 @@ from flask_socketio import join_room
|
||||
import pytz
|
||||
from flask import (
|
||||
Blueprint,
|
||||
Response,
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
@@ -24,13 +25,14 @@ from . import socketio
|
||||
|
||||
from .auth import check_password, user_loader
|
||||
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 (
|
||||
create_token,
|
||||
generate_password,
|
||||
get_active_banner,
|
||||
get_all_courses,
|
||||
get_available_decks,
|
||||
get_passwords,
|
||||
get_token,
|
||||
get_unlocked_decks,
|
||||
is_deck_unlocked,
|
||||
@@ -43,7 +45,7 @@ from .utils import (
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
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")
|
||||
def on_connect():
|
||||
@@ -100,7 +102,7 @@ def index():
|
||||
if not token:
|
||||
return redirect(url_for("main.set_token"))
|
||||
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'])
|
||||
def set_token():
|
||||
@@ -190,6 +192,7 @@ def switch_course(new_course: int):
|
||||
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
|
||||
@main.route('/admin/login', methods=['POST', 'GET'])
|
||||
def admin_login():
|
||||
form = LoginForm()
|
||||
@@ -214,27 +217,34 @@ def admin_login():
|
||||
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'])
|
||||
def access():
|
||||
password = db.session.execute(db.select(Password).where(Password.value == request.form.get('password'))).scalar()
|
||||
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))
|
||||
|
||||
def access_post():
|
||||
access_code = request.form.get('password')
|
||||
|
||||
if not access_code:
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
return process_access_code(access_code)
|
||||
|
||||
# Route for direct link/QR code access (GET)
|
||||
@main.route('/access/<access_code>', methods=['GET'])
|
||||
def access_link(access_code: str):
|
||||
password = db.session.execute(db.select(Password).where(Password.value == access_code)).scalar()
|
||||
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))
|
||||
|
||||
def access_get(access_code: str):
|
||||
return process_access_code(access_code)
|
||||
|
||||
|
||||
@main.route('/confused', methods=['POST'])
|
||||
@@ -248,25 +258,29 @@ def confused():
|
||||
)
|
||||
return ("", 204)
|
||||
|
||||
@main.route('/slides/<deck>/')
|
||||
@main.route('/slides/<deck>/<path:path>')
|
||||
def serve_deck(deck, path='index.html'):
|
||||
if not db.session.execute(db.select(Deck).where(Deck.deck==deck)).scalar():
|
||||
@main.route('/slides/<course_folder>/<deck>/')
|
||||
@main.route('/slides/<course_folder>/<deck>/<path:path>')
|
||||
def serve_deck(deck, course_folder, path='index.html'):
|
||||
course = db.session.execute(db.select(Course).where(Course.folder==course_folder)).scalar()
|
||||
if not course:
|
||||
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'))
|
||||
|
||||
slides_dir = current_app.config['SLIDES_DIR']
|
||||
deck_path = os.path.join(slides_dir, deck)
|
||||
slides_dir = os.path.join(current_app.config['SLIDES_DIR'], course.folder)
|
||||
deck_path = os.path.join(slides_dir, deck.deck)
|
||||
|
||||
if not os.path.exists(deck_path):
|
||||
return f"Deck '{deck}' not found.", 404
|
||||
|
||||
if slidev_all_pattern.match(request.path):
|
||||
return send_from_directory(deck_path, 'index.html')
|
||||
|
||||
if deck in ["tut-08","tut-11"] and path == 'index.html':
|
||||
return send_from_directory(deck_path, 'slides.pdf')
|
||||
return send_from_directory(deck_path, deck.index_file)
|
||||
|
||||
requested_file = os.path.join(deck_path, path)
|
||||
if os.path.isfile(requested_file):
|
||||
@@ -277,13 +291,16 @@ def serve_deck(deck, path='index.html'):
|
||||
@main.route('/admin', methods=['GET', 'POST'])
|
||||
@flask_login.login_required
|
||||
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()
|
||||
password_form = PasswordForm()
|
||||
if request.method == 'GET':
|
||||
password_form.value.data = generate_password()
|
||||
deck_form.deck.choices = get_available_decks(current_app)
|
||||
decks = db.session.scalars(db.select(Deck).order_by(Deck.name)).all()
|
||||
passwords = db.session.scalars(db.select(Password)).all()
|
||||
deck_form.deck.choices = get_available_decks(current_app, active_course)
|
||||
decks = active_course.decks
|
||||
password_form.decks.choices = [(deck.id, deck.name) for deck in decks]
|
||||
|
||||
|
||||
@@ -292,6 +309,7 @@ def admin():
|
||||
deck = Deck(
|
||||
name=cast(str,deck_form.name.data),
|
||||
deck=deck_form.deck.data,
|
||||
course=active_course,
|
||||
description=cast(str,deck_form.description.data),
|
||||
)
|
||||
db.session.add(deck)
|
||||
@@ -320,7 +338,15 @@ def admin():
|
||||
print(f"Error in {getattr(password_form, field).label.text}: {error}", flush=True)
|
||||
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'])
|
||||
@flask_login.login_required
|
||||
@@ -340,7 +366,6 @@ def delete_deck(deck_id: int):
|
||||
deck = Deck.query.get_or_404(deck_id)
|
||||
db.session.delete(deck)
|
||||
db.session.commit()
|
||||
flash(f'Deck "{deck.name}" deleted.', 'success')
|
||||
return redirect(url_for('main.admin'))
|
||||
|
||||
@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)
|
||||
db.session.delete(password)
|
||||
db.session.commit()
|
||||
flash(f'Password "{password.value}" deleted.', 'success')
|
||||
return redirect(url_for('main.admin'))
|
||||
|
||||
@main.route('/admin/banner/show/<int:banner_id>', methods=['POST'])
|
||||
|
||||
26
app/seed_db.py
Normal file
26
app/seed_db.py
Normal 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()
|
||||
@@ -34,26 +34,53 @@
|
||||
</head>
|
||||
<body class="bg-background text-text min-h-screen font-sans">
|
||||
<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">
|
||||
<a href="{{url_for('main.index')}}" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
<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>
|
||||
</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">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</nav>
|
||||
<div class="container mx-auto px-6 py-10 space-y-12">
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10">
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
|
||||
<!-- 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://unpkg.com/htmx.org@1.9.10"></script>
|
||||
|
||||
|
||||
</head>
|
||||
@@ -41,66 +43,35 @@
|
||||
|
||||
<!-- Header -->
|
||||
<header class="bg-surface border-b border-border shadow-sm">
|
||||
<div
|
||||
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="w-full px-6 py-4 flex items-center justify-between" >
|
||||
|
||||
<div class="hamburger-menu">
|
||||
<button class="menu-toggle" aria-controls="course-selector">Menu</button>
|
||||
<div class="flex items-center space-x-3">
|
||||
<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()">
|
||||
<option value="" disabled selected>Select a Course</option>
|
||||
|
||||
{% for course in courses %}
|
||||
<option value="{{ course.id }}">
|
||||
{{ course.display_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
</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">
|
||||
<button
|
||||
type="submit"
|
||||
class="text-sm text-white bg-red-600 px-3 py-1 rounded-md hover:bg-red-700 transition"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="/logout">
|
||||
<button
|
||||
type="submit"
|
||||
class="text-sm text-white bg-red-600 px-3 py-1 rounded-md hover:bg-red-700 transition"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</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 class="relative z-10">
|
||||
<a href="/admin">
|
||||
<button class="bg-primary hover:bg-blue-700 text-white px-3 py-1 rounded-md transition">
|
||||
Admin
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
document.getElementById('menu-toggle').addEventListener('click', function() {
|
||||
document.getElementById('course-menu').classList.toggle('hidden');
|
||||
});
|
||||
</script> <!-- Main Content -->
|
||||
</header> <!-- Main Content -->
|
||||
|
||||
<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 %}
|
||||
<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">
|
||||
@@ -109,6 +80,33 @@
|
||||
</section>
|
||||
{% 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 -->
|
||||
<section class="flex justify-center">
|
||||
<div class="w-full max-w-md">
|
||||
@@ -130,9 +128,21 @@
|
||||
Unlock
|
||||
</button>
|
||||
</form>
|
||||
{% if error %}
|
||||
<p class="text-red-500 text-center mt-3">{{ error }}</p>
|
||||
{% endif %}
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<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>
|
||||
</section>
|
||||
@@ -142,7 +152,7 @@
|
||||
<section>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% 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>
|
||||
<p class="text-muted text-sm">{{ deck.description }}</p>
|
||||
</a>
|
||||
|
||||
12
app/utils.py
12
app/utils.py
@@ -19,8 +19,8 @@ def init_serializer(app: Flask):
|
||||
def get_active_banner() -> InfoBanner | None:
|
||||
return db.session.execute(db.select(InfoBanner).where(InfoBanner.is_active)).scalar_one_or_none()
|
||||
|
||||
def get_available_decks(app: Flask):
|
||||
slides_dir = app.config['SLIDES_DIR']
|
||||
def get_available_decks(app: Flask, course: Course):
|
||||
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.sort()
|
||||
return deck_names
|
||||
@@ -31,6 +31,9 @@ def generate_username() -> str:
|
||||
def generate_password() -> str:
|
||||
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:
|
||||
max_tries = 100
|
||||
for _ in range(max_tries):
|
||||
@@ -68,12 +71,13 @@ def retrieve_token(username : str):
|
||||
def get_unlocked_decks(token: (Token|None)):
|
||||
if token is None:
|
||||
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)
|
||||
return decks
|
||||
|
||||
|
||||
def is_deck_unlocked(deck: str):
|
||||
token = get_token()
|
||||
print(token)
|
||||
if token:
|
||||
return token.is_unlocked(deck)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user