diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 3e5b861..0000000 --- a/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -.venv -__pycache__/ -*.pyc -nohup.out -.git -log/ - diff --git a/.gitignore b/.gitignore index 5783508..51eb369 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,7 @@ instance __pycache__ node_modules .venv -config.py -slides -exports +data logs nohup.out +.env* diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..2de8a87 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,41 @@ +# ========================================== +# Stage 1: Build the CSS using Node.js +# ========================================== +FROM node:20-slim AS css-builder + +WORKDIR /build + +# Copy package files and install dependencies +COPY package*.json ./ +RUN npm install + +# Copy your source code (needed if your CSS framework scans HTML/JS for classes, like Tailwind) +COPY . . + +# Run your build script +RUN npm run build:css + +# ========================================== +# Stage 2: Final Python Image +# ========================================== +FROM python:3.13-slim + +WORKDIR /src + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the entire project from your host machine +COPY . ./app/ + +# Overwrite the empty CSS directory with the compiled CSS from Stage 1 +# IMPORTANT: Change the paths below to match where your CSS actually gets generated! +# Example: COPY --from=css-builder /build/static/css/style.css ./app/static/css/style.css +COPY --from=css-builder /build/static/css/output.css ./app/static/css/output.css + +# Expose Gunicorn port +EXPOSE 5000 + +# Gunicorn command +CMD ["gunicorn", "--preload", "-w", "4", "-b", "0.0.0.0:5000", "app:create_app()"] diff --git a/app/__init__.py b/app/__init__.py index 5783d57..1918b29 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,35 +3,37 @@ import logging from logging.handlers import WatchedFileHandler from flask import Flask 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 +from app.models import db +from app.utils import init_password_generator, init_serializer +from app.auth import init_auth +from app.seed_db import seed_courses +from app.config import Config socketio = SocketIO(async_mode='eventlet', cors_allowed_origins="*") -def create_app(): +def create_app(config_class=Config): # 1. Setup Log Directory log_dir = os.path.join(os.getcwd(), 'logs') if not os.path.exists(log_dir): os.makedirs(log_dir) app = Flask(__name__, static_folder='static', static_url_path='/static') - app.config.from_object('config.Config') + app.config.from_object(config_class) file_handler = WatchedFileHandler(log_dir + '/app.log', mode='a', encoding='UTF-8') file_handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s [%(module)s:%(lineno)d]: %(message)s' )) - file_handler.setLevel(logging.INFO) + file_handler.setLevel(logging.DEBUG) app.logger.addHandler(file_handler) - app.logger.setLevel(logging.INFO) + app.logger.setLevel(logging.DEBUG) db.init_app(app) with app.app_context(): db.create_all() seed_courses() + db.engine.dispose() init_password_generator() init_serializer(app) diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..0507eea --- /dev/null +++ b/app/config.py @@ -0,0 +1,15 @@ +import os + + +class Config: + SECRET_KEY = os.environ.get("SECRET_KEY") + SLIDES_DIR = os.environ.get("SLIDES_DIR") + EXPORTS_DIR = os.environ.get("EXPORTS_DIR") + SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") + SQLALCHEMY_TRACK_MODIFICATIONS = False + ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD") + COURSE_MAP = { + "GRNVS25": "GRNVS Tutorium SS 25", + "IT-Sec25": "IT-Sec Tutorium WS 25/26", + } + DEFAULT_COURSE = 2 diff --git a/package-lock.json b/app/package-lock.json similarity index 100% rename from package-lock.json rename to app/package-lock.json diff --git a/package.json b/app/package.json similarity index 81% rename from package.json rename to app/package.json index 6ceaaa9..46780a0 100644 --- a/package.json +++ b/app/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "build:css": "npx tailwindcss -i app/static/css/input.css -o app/static/css/output.css --minify" + "build:css": "npx tailwindcss -i static/css/input.css -o static/css/output.css --minify" }, "repository": { "type": "git", diff --git a/postcss.config.js b/app/postcss.config.js similarity index 100% rename from postcss.config.js rename to app/postcss.config.js diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..482ba45 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,83 @@ +# This file was autogenerated by uv via the following command: +# uv export --format requirements-txt -o requirements.txt --no-hashes +bidict==0.23.1 + # via python-socketio +blinker==1.9.0 + # via flask +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 +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 +flask==3.1.2 + # via + # flask-login + # flask-socketio + # flask-sqlalchemy + # flask-wtf + # tutor-tool +flask-login==0.6.3 + # via 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 +greenlet==3.2.4 + # via + # eventlet + # sqlalchemy +gunicorn==23.0.0 + # via tutor-tool +h11==0.16.0 + # via wsproto +importlib-metadata==8.7.0 ; python_full_version < '3.10' + # via flask +itsdangerous==2.2.0 + # via + # flask + # flask-wtf +jinja2==3.1.6 + # via flask +markupsafe==3.0.2 + # via + # flask + # jinja2 + # werkzeug + # wtforms +packaging==25.0 + # via gunicorn +psycopg2-binary==2.9.11 + # via tutor-tool +python-engineio==4.12.2 + # via python-socketio +python-socketio==5.13.0 + # via flask-socketio +pytz==2025.2 + # via tutor-tool +simple-websocket==1.1.0 + # via python-engineio +sqlalchemy==2.0.43 + # via flask-sqlalchemy +typing-extensions==4.15.0 + # via sqlalchemy +werkzeug==3.1.3 + # via + # flask + # flask-login +wsproto==1.2.0 + # via simple-websocket +wtforms==3.2.1 + # via flask-wtf +xkcdpass==1.20.0 + # via tutor-tool +zipp==3.23.0 ; python_full_version < '3.10' + # via importlib-metadata diff --git a/app/routes.py b/app/routes.py index 532c432..8cc1f7b 100644 --- a/app/routes.py +++ b/app/routes.py @@ -10,7 +10,7 @@ import pytz from flask import ( Blueprint, abort, - current_app, # Used for logging + current_app, # Used for logging flash, make_response, redirect, @@ -35,31 +35,44 @@ from .utils import ( get_passwords, get_token, get_unlocked_decks, - is_deck_unlocked, retrieve_token, serialize_token, update_token, - get_course + get_course, + token_required, + get_cached_deck_info, ) -main = Blueprint('main', __name__) -slidev_deck_pattern = re.compile(r"^\/slides\/[^\/]+\/[^\/]+\/(?:\d+|index\.html)(?:\?.*)?$") -slidev_all_pattern = re.compile(r"^\/slides\/([^\/]+)\/([^\/]+)(?:\/presenter)?\/(\d+)(?:\?.*)?$") +main = Blueprint("main", __name__) +slidev_deck_pattern = re.compile( + r"^\/slides\/[^\/]+\/[^\/]+\/(?:\d+|index\.html)(?:\?.*)?$" +) +slidev_all_pattern = re.compile( + r"^\/slides\/([^\/]+)\/([^\/]+)(?:\/presenter)?\/(\d+)(?:\?.*)?$" +) + @socketio.on("connect") def on_connect(): if flask_login.current_user.is_authenticated: join_room("instructor") - current_app.logger.info(f"Instructor connected to socket: {flask_login.current_user.id}") + current_app.logger.info( + f"Instructor connected to socket: {flask_login.current_user.id}" + ) + @main.after_request def inject_confused_button(response): - if (slidev_deck_pattern.match(request.path) or request.path == '/') and response.content_type.startswith('text/html') and not flask_login.current_user.is_authenticated : + if ( + (slidev_deck_pattern.match(request.path) or request.path == "/") + and response.content_type.startswith("text/html") + and not flask_login.current_user.is_authenticated + ): if response.direct_passthrough: response.direct_passthrough = False body = response.get_data(as_text=True) - insert_at = body.find('') + insert_at = body.find("") if insert_at != -1: button_html = """