diff --git a/elearn/README.md b/elearn/README.md new file mode 100644 index 0000000..86c4fc9 --- /dev/null +++ b/elearn/README.md @@ -0,0 +1,36 @@ +# E‑Learn (Flask + SQLite) + +A minimal e‑learning app: +- User auth (email/password) +- Courses with lessons (optionally with embedded video URLs) +- Enrollments and progress tracking +- Quizzes with multiple‑choice questions and scoring +- Simple Admin to create courses, lessons, and quizzes + +## Quickstart + +1. Create and activate a virtualenv (recommended): + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +2. Install dependencies: + +```bash +pip install -r requirements.txt +``` + +3. Run the app: + +```bash +python app.py +``` + +The app will be available at http://localhost:5000 + +## Notes +- Database file: `elearn.db` (auto-created on first request) +- Default roles: users register as `student`. To make an admin, update the `users.role` column to `admin` using any SQLite tool. +- Video: for YouTube embeds, paste an embed URL like `https://www.youtube.com/embed/VIDEO_ID`. \ No newline at end of file diff --git a/elearn/__pycache__/db.cpython-313.pyc b/elearn/__pycache__/db.cpython-313.pyc new file mode 100644 index 0000000..fbe8eac Binary files /dev/null and b/elearn/__pycache__/db.cpython-313.pyc differ diff --git a/elearn/app.py b/elearn/app.py new file mode 100644 index 0000000..59bb607 --- /dev/null +++ b/elearn/app.py @@ -0,0 +1,247 @@ +import os +from typing import Dict, Optional +from flask import Flask, render_template, request, redirect, url_for, session, flash, abort +from werkzeug.security import generate_password_hash, check_password_hash + +import db + + +def create_app() -> Flask: + app = Flask(__name__, template_folder='templates', static_folder='static') + app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-change-me') + + # Ensure database exists + @app.before_request + def _ensure_db(): + db.initialize_database() + + def current_user() -> Optional[dict]: + user_id = session.get('user_id') + if not user_id: + return None + user_row = db.get_user_by_id(user_id) + if not user_row: + return None + return dict(user_row) + + def login_required(): + if not session.get('user_id'): + return redirect(url_for('login', next=request.path)) + return None + + def admin_required(): + user = current_user() + if not user or user.get('role') != 'admin': + abort(403) + + @app.route('/') + def index(): + user = current_user() + courses = db.list_courses() + return render_template('index.html', user=user, courses=courses) + + @app.route('/register', methods=['GET', 'POST']) + def register(): + if request.method == 'POST': + name = request.form.get('name', '').strip() + email = request.form.get('email', '').strip().lower() + password = request.form.get('password', '') + + if not name or not email or not password: + flash('All fields are required.', 'error') + return redirect(url_for('register')) + + existing = db.get_user_by_email(email) + if existing: + flash('Email is already registered.', 'error') + return redirect(url_for('register')) + + password_hash = generate_password_hash(password) + user_id = db.create_user(email=email, name=name, password_hash=password_hash) + session['user_id'] = user_id + session['role'] = 'student' + flash('Welcome! Your account has been created.', 'success') + return redirect(url_for('dashboard')) + + return render_template('register.html') + + @app.route('/login', methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + email = request.form.get('email', '').strip().lower() + password = request.form.get('password', '') + next_url = request.args.get('next') or url_for('dashboard') + user_row = db.get_user_by_email(email) + if not user_row or not check_password_hash(user_row['password_hash'], password): + flash('Invalid credentials.', 'error') + return redirect(url_for('login')) + session['user_id'] = user_row['id'] + session['role'] = user_row['role'] + flash('Logged in successfully.', 'success') + return redirect(next_url) + return render_template('login.html') + + @app.route('/logout') + def logout(): + session.clear() + flash('Logged out.', 'info') + return redirect(url_for('index')) + + @app.route('/dashboard') + def dashboard(): + redirect_response = login_required() + if redirect_response: + return redirect_response + user = current_user() + enrolled_courses = db.list_user_enrollments(user_id=user['id']) + progress_by_course: Dict[int, tuple] = {} + for course in enrolled_courses: + progress_by_course[course['id']] = db.get_course_progress(user['id'], course['id']) + return render_template('dashboard.html', user=user, enrolled_courses=enrolled_courses, progress_by_course=progress_by_course) + + @app.route('/courses') + def courses(): + user = current_user() + courses_list = db.list_courses() + return render_template('courses.html', user=user, courses=courses_list) + + @app.route('/courses/') + def course_detail(course_id: int): + user = current_user() + course = db.get_course(course_id) + if not course: + abort(404) + lessons = db.list_lessons_for_course(course_id) + completed, total = (0, len(lessons)) + if user: + completed, total = db.get_course_progress(user['id'], course_id) + return render_template('course_detail.html', user=user, course=course, lessons=lessons, progress=(completed, total)) + + @app.route('/courses//enroll', methods=['POST']) + def enroll(course_id: int): + redirect_response = login_required() + if redirect_response: + return redirect_response + user = current_user() + if not db.get_course(course_id): + abort(404) + db.enroll_user_in_course(user_id=user['id'], course_id=course_id) + flash('Enrolled in course.', 'success') + return redirect(url_for('course_detail', course_id=course_id)) + + @app.route('/lessons/') + def lesson_detail(lesson_id: int): + user = current_user() + lesson = db.get_lesson(lesson_id) + if not lesson: + abort(404) + course = db.get_course(lesson['course_id']) + quiz = db.get_quiz_by_lesson(lesson_id) + latest_attempt = None + if user and quiz: + latest_attempt = db.get_latest_quiz_attempt(user['id'], quiz['id']) + return render_template('lesson_detail.html', user=user, lesson=lesson, course=course, quiz=quiz, latest_attempt=latest_attempt) + + @app.route('/lessons//complete', methods=['POST']) + def complete_lesson(lesson_id: int): + redirect_response = login_required() + if redirect_response: + return redirect_response + user = current_user() + if not db.get_lesson(lesson_id): + abort(404) + db.mark_lesson_completed(user['id'], lesson_id) + flash('Lesson marked complete.', 'success') + lesson = db.get_lesson(lesson_id) + return redirect(url_for('course_detail', course_id=lesson['course_id'])) + + @app.route('/quiz/', methods=['GET', 'POST']) + def quiz_take(quiz_id: int): + redirect_response = login_required() + if redirect_response: + return redirect_response + user = current_user() + quiz = db.get_quiz(quiz_id) + if not quiz: + abort(404) + questions = db.get_questions_for_quiz(quiz_id) + + if request.method == 'POST': + score = 0 + total = len(questions) + for q in questions: + chosen = request.form.get(f'q{q["id"]}') + if chosen and chosen == q['correct_choice']: + score += 1 + db.record_quiz_attempt(user['id'], quiz_id, score, total) + flash(f'Quiz submitted. Score: {score}/{total}', 'success') + return redirect(url_for('lesson_detail', lesson_id=quiz['lesson_id'])) + + return render_template('quiz.html', user=user, quiz=quiz, questions=questions) + + @app.route('/admin', methods=['GET', 'POST']) + def admin(): + redirect_response = login_required() + if redirect_response: + return redirect_response + admin_required() + + if request.method == 'POST': + action = request.form.get('action') + + if action == 'create_course': + title = request.form.get('title', '').strip() + description = request.form.get('description', '').strip() + if title and description: + db.create_course(title, description) + flash('Course created.', 'success') + else: + flash('Title and description required.', 'error') + + elif action == 'create_lesson': + course_id = int(request.form.get('course_id', '0')) + title = request.form.get('lesson_title', '').strip() + content = request.form.get('content', '').strip() + video_url = request.form.get('video_url', '').strip() or None + order_index = int(request.form.get('order_index', '0')) + if db.get_course(course_id) and title and content: + db.create_lesson(course_id, title, content, video_url, order_index) + flash('Lesson created.', 'success') + else: + flash('Valid course, title, and content required.', 'error') + + elif action == 'create_quiz': + lesson_id = int(request.form.get('lesson_id', '0')) + title = request.form.get('quiz_title', '').strip() + if db.get_lesson(lesson_id) and title: + quiz_id = db.create_quiz(lesson_id, title) + # Optional: add one sample question if provided + q_text = request.form.get('q_text', '').strip() + a = request.form.get('choice_a', '').strip() + b = request.form.get('choice_b', '').strip() + c = request.form.get('choice_c', '').strip() + d = request.form.get('choice_d', '').strip() + correct = request.form.get('correct_choice', 'a').strip() + if q_text and a and b and c and d and correct in {'a', 'b', 'c', 'd'}: + db.add_question(quiz_id, q_text, a, b, c, d, correct) + flash('Quiz created.', 'success') + else: + flash('Valid lesson and title required.', 'error') + + courses_list = db.list_courses() + selected_course_id = request.args.get('course_id') + lessons_for_selected = [] + if selected_course_id: + try: + lessons_for_selected = db.list_lessons_for_course(int(selected_course_id)) + except Exception: + lessons_for_selected = [] + return render_template('admin.html', user=current_user(), courses=courses_list, lessons=lessons_for_selected) + + return app + + +if __name__ == '__main__': + application = create_app() + port = int(os.environ.get('PORT', '5000')) + application.run(host='0.0.0.0', port=port, debug=True) \ No newline at end of file diff --git a/elearn/db.py b/elearn/db.py new file mode 100644 index 0000000..5bae46a --- /dev/null +++ b/elearn/db.py @@ -0,0 +1,275 @@ +import os +import sqlite3 +from typing import Any, Dict, List, Optional, Tuple + +DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'elearn.db') +SCHEMA_PATH = os.path.join(os.path.dirname(__file__), 'schema.sql') + + +def get_connection() -> sqlite3.Connection: + connection = sqlite3.connect(DATABASE_PATH) + connection.row_factory = sqlite3.Row + return connection + + +def initialize_database() -> None: + if not os.path.exists(DATABASE_PATH): + connection = get_connection() + try: + with open(SCHEMA_PATH, 'r', encoding='utf-8') as schema_file: + connection.executescript(schema_file.read()) + connection.commit() + finally: + connection.close() + + +# User functions + +def create_user(email: str, name: str, password_hash: str, role: str = 'student') -> int: + connection = get_connection() + try: + cursor = connection.execute( + 'INSERT INTO users (email, name, password_hash, role) VALUES (?, ?, ?, ?)', + (email, name, password_hash, role), + ) + connection.commit() + return cursor.lastrowid + finally: + connection.close() + + +def get_user_by_email(email: str) -> Optional[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute('SELECT * FROM users WHERE email = ?', (email,)) + return cursor.fetchone() + finally: + connection.close() + + +def get_user_by_id(user_id: int) -> Optional[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute('SELECT * FROM users WHERE id = ?', (user_id,)) + return cursor.fetchone() + finally: + connection.close() + + +# Courses + +def create_course(title: str, description: str) -> int: + connection = get_connection() + try: + cursor = connection.execute( + 'INSERT INTO courses (title, description) VALUES (?, ?)', (title, description) + ) + connection.commit() + return cursor.lastrowid + finally: + connection.close() + + +def list_courses() -> List[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute('SELECT * FROM courses ORDER BY created_at DESC') + return cursor.fetchall() + finally: + connection.close() + + +def get_course(course_id: int) -> Optional[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute('SELECT * FROM courses WHERE id = ?', (course_id,)) + return cursor.fetchone() + finally: + connection.close() + + +# Lessons + +def create_lesson( + course_id: int, + title: str, + content: str, + video_url: Optional[str], + order_index: int = 0, +) -> int: + connection = get_connection() + try: + cursor = connection.execute( + 'INSERT INTO lessons (course_id, title, content, video_url, order_index) VALUES (?, ?, ?, ?, ?)', + (course_id, title, content, video_url, order_index), + ) + connection.commit() + return cursor.lastrowid + finally: + connection.close() + + +def list_lessons_for_course(course_id: int) -> List[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute( + 'SELECT * FROM lessons WHERE course_id = ? ORDER BY order_index ASC, id ASC', (course_id,) + ) + return cursor.fetchall() + finally: + connection.close() + + +def get_lesson(lesson_id: int) -> Optional[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute('SELECT * FROM lessons WHERE id = ?', (lesson_id,)) + return cursor.fetchone() + finally: + connection.close() + + +# Enrollments + +def enroll_user_in_course(user_id: int, course_id: int) -> None: + connection = get_connection() + try: + connection.execute( + 'INSERT OR IGNORE INTO enrollments (user_id, course_id) VALUES (?, ?)', (user_id, course_id) + ) + connection.commit() + finally: + connection.close() + + +def list_user_enrollments(user_id: int) -> List[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute( + 'SELECT c.* FROM courses c JOIN enrollments e ON c.id = e.course_id WHERE e.user_id = ? ORDER BY c.created_at DESC', + (user_id,), + ) + return cursor.fetchall() + finally: + connection.close() + + +# Progress + +def mark_lesson_completed(user_id: int, lesson_id: int) -> None: + connection = get_connection() + try: + connection.execute( + 'INSERT OR IGNORE INTO progress (user_id, lesson_id) VALUES (?, ?)', (user_id, lesson_id) + ) + connection.commit() + finally: + connection.close() + + +def get_course_progress(user_id: int, course_id: int) -> Tuple[int, int]: + """ + Returns (completed_count, total_lessons) for the given course. + """ + connection = get_connection() + try: + total_cursor = connection.execute( + 'SELECT COUNT(*) as total FROM lessons WHERE course_id = ?', (course_id,) + ) + total_lessons = total_cursor.fetchone()['total'] + + completed_cursor = connection.execute( + 'SELECT COUNT(*) as completed FROM progress p JOIN lessons l ON p.lesson_id = l.id WHERE p.user_id = ? AND l.course_id = ?', + (user_id, course_id), + ) + completed = completed_cursor.fetchone()['completed'] + return completed, total_lessons + finally: + connection.close() + + +# Quizzes + +def create_quiz(lesson_id: int, title: str) -> int: + connection = get_connection() + try: + cursor = connection.execute( + 'INSERT INTO quizzes (lesson_id, title) VALUES (?, ?)', (lesson_id, title) + ) + connection.commit() + return cursor.lastrowid + finally: + connection.close() + + +def add_question( + quiz_id: int, + question_text: str, + choice_a: str, + choice_b: str, + choice_c: str, + choice_d: str, + correct_choice: str, +) -> int: + connection = get_connection() + try: + cursor = connection.execute( + 'INSERT INTO questions (quiz_id, question_text, choice_a, choice_b, choice_c, choice_d, correct_choice) VALUES (?, ?, ?, ?, ?, ?, ?)', + (quiz_id, question_text, choice_a, choice_b, choice_c, choice_d, correct_choice), + ) + connection.commit() + return cursor.lastrowid + finally: + connection.close() + + +def get_quiz_by_lesson(lesson_id: int) -> Optional[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute('SELECT * FROM quizzes WHERE lesson_id = ?', (lesson_id,)) + return cursor.fetchone() + finally: + connection.close() + + +def get_quiz(quiz_id: int) -> Optional[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute('SELECT * FROM quizzes WHERE id = ?', (quiz_id,)) + return cursor.fetchone() + finally: + connection.close() + + +def get_questions_for_quiz(quiz_id: int) -> List[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute('SELECT * FROM questions WHERE quiz_id = ? ORDER BY id ASC', (quiz_id,)) + return cursor.fetchall() + finally: + connection.close() + + +def record_quiz_attempt(user_id: int, quiz_id: int, score: int, total: int) -> int: + connection = get_connection() + try: + cursor = connection.execute( + 'INSERT INTO quiz_attempts (user_id, quiz_id, score, total) VALUES (?, ?, ?, ?)', + (user_id, quiz_id, score, total), + ) + connection.commit() + return cursor.lastrowid + finally: + connection.close() + + +def get_latest_quiz_attempt(user_id: int, quiz_id: int) -> Optional[sqlite3.Row]: + connection = get_connection() + try: + cursor = connection.execute( + 'SELECT * FROM quiz_attempts WHERE user_id = ? AND quiz_id = ? ORDER BY taken_at DESC, id DESC LIMIT 1', + (user_id, quiz_id), + ) + return cursor.fetchone() + finally: + connection.close() \ No newline at end of file diff --git a/elearn/elearn.db b/elearn/elearn.db new file mode 100644 index 0000000..e26f121 Binary files /dev/null and b/elearn/elearn.db differ diff --git a/elearn/requirements.txt b/elearn/requirements.txt new file mode 100644 index 0000000..a993b8d --- /dev/null +++ b/elearn/requirements.txt @@ -0,0 +1 @@ +Flask==3.0.3 \ No newline at end of file diff --git a/elearn/schema.sql b/elearn/schema.sql new file mode 100644 index 0000000..7245680 --- /dev/null +++ b/elearn/schema.sql @@ -0,0 +1,77 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'student', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS courses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS lessons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + course_id INTEGER NOT NULL, + title TEXT NOT NULL, + content TEXT NOT NULL, + video_url TEXT, + order_index INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS enrollments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + course_id INTEGER NOT NULL, + enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, course_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS progress ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + lesson_id INTEGER NOT NULL, + completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, lesson_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS quizzes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + lesson_id INTEGER NOT NULL, + title TEXT NOT NULL, + FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS questions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + quiz_id INTEGER NOT NULL, + question_text TEXT NOT NULL, + choice_a TEXT NOT NULL, + choice_b TEXT NOT NULL, + choice_c TEXT NOT NULL, + choice_d TEXT NOT NULL, + correct_choice TEXT NOT NULL CHECK (correct_choice IN ('a', 'b', 'c', 'd')), + FOREIGN KEY (quiz_id) REFERENCES quizzes(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS quiz_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + quiz_id INTEGER NOT NULL, + score INTEGER NOT NULL, + total INTEGER NOT NULL, + taken_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (quiz_id) REFERENCES quizzes(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/elearn/static/app.js b/elearn/static/app.js new file mode 100644 index 0000000..dac69e6 --- /dev/null +++ b/elearn/static/app.js @@ -0,0 +1,2 @@ +// Placeholder for client-side enhancements (e.g., fetch actions, toasts) +console.debug('E‑Learn app loaded'); \ No newline at end of file diff --git a/elearn/static/style.css b/elearn/static/style.css new file mode 100644 index 0000000..e900a1c --- /dev/null +++ b/elearn/static/style.css @@ -0,0 +1,32 @@ +:root{--bg:#0b0c10;--panel:#12151b;--muted:#9aa3b2;--text:#e8edf3;--brand:#6aa1ff;--brand-2:#a66bff;--ok:#10b981;--warn:#f59e0b;--err:#ef4444} +*{box-sizing:border-box}body{margin:0;background:linear-gradient(180deg,var(--bg),#0d1117);color:var(--text);font:16px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Inter,system-ui,Arial} +a{color:var(--brand);text-decoration:none}a:hover{text-decoration:underline} +.container{max-width:1000px;margin:0 auto;padding:24px} +.header-inner{display:flex;align-items:center;justify-content:space-between} +.site-header{position:sticky;top:0;background:rgba(13,17,23,.8);backdrop-filter:saturate(1.2) blur(8px);border-bottom:1px solid #1f2937} +.logo{font-weight:700;letter-spacing:.2px} +nav a{margin-left:16px} +.hero{padding:24px 0 8px} +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px} +.grid.two{grid-template-columns:repeat(2,1fr)} +.card{background:linear-gradient(180deg,var(--panel),#0e1016);border:1px solid #1f2937;border-radius:12px;padding:16px} +.btn,.btn-primary{display:inline-block;padding:10px 14px;border-radius:10px;border:1px solid #334155} +.btn:hover,.btn-primary:hover{transform:translateY(-1px)} +.btn-primary{background:linear-gradient(135deg,var(--brand),var(--brand-2));color:#fff;border:0} +.form{display:grid;gap:12px;max-width:520px} +.form input,.form textarea,.form select{width:100%;padding:10px;border-radius:8px;border:1px solid #334155;background:#0b1020;color:var(--text)} +.muted{color:var(--muted)} +.flash-list{list-style:none;padding:0;margin:16px 0;display:grid;gap:8px} +.flash{padding:10px 12px;border-radius:10px} +.flash.success{background:#052e1a;border:1px solid #064e3b} +.flash.error{background:#3b0a0a;border:1px solid #7f1d1d} +.flash.info{background:#0a1f3b;border:1px solid #1d4ed8} +.progress{height:8px;background:#111827;border-radius:6px;overflow:hidden;border:1px solid #1f2937;margin:8px 0} +.progress>span{display:block;height:100%;background:linear-gradient(90deg,var(--brand),var(--brand-2))} +.lesson-list{padding-left:18px} +.video{aspect-ratio:16/9;background:#0b1020;border:1px solid #1f2937;border-radius:12px;overflow:hidden;margin:12px 0} +.video iframe{width:100%;height:100%} +.site-footer{border-top:1px solid #1f2937;margin-top:24px} +.stack{margin:24px 0} +.choice{display:flex;gap:8px;margin:4px 0} +.question{border:1px solid #1f2937;border-radius:10px;padding:12px;margin:8px 0;background:#0b1020} \ No newline at end of file diff --git a/elearn/templates/admin.html b/elearn/templates/admin.html new file mode 100644 index 0000000..0f3c72b --- /dev/null +++ b/elearn/templates/admin.html @@ -0,0 +1,88 @@ +{% extends 'base.html' %} +{% block title %}Admin · E‑Learn{% endblock %} +{% block content %} +

Admin

+

Create courses, lessons, and quizzes.

+ +
+

Create course

+
+ + + + +
+
+ +
+

Create lesson

+
+ + + + + + + +
+
+ +
+

Create quiz + first question (optional)

+
+ +

Select from preloaded lessons (based on ?course_id=ID query):

+ + + +
+ Add first question (optional) + +
+ + + + +
+ +
+ + +
+
+{% endblock %} \ No newline at end of file diff --git a/elearn/templates/base.html b/elearn/templates/base.html new file mode 100644 index 0000000..023ee25 --- /dev/null +++ b/elearn/templates/base.html @@ -0,0 +1,45 @@ + + + + + + {% block title %}E-Learn{% endblock %} + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    + {% for category, message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ +
+
© {{ 2025 }} E‑Learn
+
+ + + \ No newline at end of file diff --git a/elearn/templates/course_detail.html b/elearn/templates/course_detail.html new file mode 100644 index 0000000..c2af2a9 --- /dev/null +++ b/elearn/templates/course_detail.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} +{% block title %}{{ course.title }} · E‑Learn{% endblock %} +{% block content %} +

{{ course.title }}

+

{{ course.description }}

+ +{% if user %} +
+ +
+{% else %} +

Login to enroll.

+{% endif %} + +

Lessons

+
    + {% for lesson in lessons %} +
  1. + {{ lesson.title }} +
  2. + {% else %} +
  3. No lessons yet.
  4. + {% endfor %} +
+ +{% if user %} + {% set completed, total = progress %} +

Progress: {{ completed }}/{{ total }}

+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/elearn/templates/courses.html b/elearn/templates/courses.html new file mode 100644 index 0000000..3d1ec8a --- /dev/null +++ b/elearn/templates/courses.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% block title %}Courses · E‑Learn{% endblock %} +{% block content %} +

All courses

+
+ {% for course in courses %} +
+

{{ course.title }}

+

{{ course.description[:160] }}{% if course.description|length > 160 %}…{% endif %}

+ View +
+ {% else %} +

No courses yet.

+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/elearn/templates/dashboard.html b/elearn/templates/dashboard.html new file mode 100644 index 0000000..bf0e638 --- /dev/null +++ b/elearn/templates/dashboard.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% block title %}Dashboard · E‑Learn{% endblock %} +{% block content %} +

Welcome, {{ user.name }}!

+ +

Your courses

+
+ {% for course in enrolled_courses %} + {% set progress = progress_by_course.get(course.id, (0, 0)) %} +
+

{{ course.title }}

+

{{ course.description[:140] }}{% if course.description|length > 140 %}…{% endif %}

+
+ Go to course +
+ {% else %} +

You are not enrolled in any courses yet. Browse all courses.

+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/elearn/templates/index.html b/elearn/templates/index.html new file mode 100644 index 0000000..3357b92 --- /dev/null +++ b/elearn/templates/index.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% block title %}E‑Learn · Home{% endblock %} +{% block content %} +
+

Learn anything, anytime.

+

Simple, open‑source e‑learning platform. Create courses, publish lessons, take quizzes.

+ {% if not user %} +

Get started

+ {% endif %} +
+ +
+

Featured courses

+
+ {% for course in courses %} +
+

{{ course.title }}

+

{{ course.description[:160] }}{% if course.description|length > 160 %}…{% endif %}

+ View course +
+ {% else %} +

No courses yet. {% if user and user.role == 'admin' %}Head to Admin to create one.{% endif %}

+ {% endfor %} +
+
+{% endblock %} \ No newline at end of file diff --git a/elearn/templates/lesson_detail.html b/elearn/templates/lesson_detail.html new file mode 100644 index 0000000..1658ebc --- /dev/null +++ b/elearn/templates/lesson_detail.html @@ -0,0 +1,34 @@ +{% extends 'base.html' %} +{% block title %}{{ lesson.title }} · {{ course.title }} · E‑Learn{% endblock %} +{% block content %} +← Back to course +

{{ lesson.title }}

+ +{% if lesson.video_url %} +
+ +
+{% endif %} + +
{{ lesson.content | safe }}
+ +{% if user %} +
+ +
+{% else %} +

Login to track your progress.

+{% endif %} + +{% if quiz %} +

Quiz: {{ quiz.title }}

+ {% if latest_attempt %} +

Latest attempt: {{ latest_attempt.score }}/{{ latest_attempt.total }} on {{ latest_attempt.taken_at }}

+ {% endif %} + {% if user %} + Take quiz + {% else %} +

Login to take the quiz.

+ {% endif %} +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/elearn/templates/login.html b/elearn/templates/login.html new file mode 100644 index 0000000..5133d4a --- /dev/null +++ b/elearn/templates/login.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block title %}Login · E‑Learn{% endblock %} +{% block content %} +

Login

+
+ + + +
+

New here? Create an account.

+{% endblock %} \ No newline at end of file diff --git a/elearn/templates/quiz.html b/elearn/templates/quiz.html new file mode 100644 index 0000000..49208ad --- /dev/null +++ b/elearn/templates/quiz.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} +{% block title %}{{ quiz.title }} · Quiz · E‑Learn{% endblock %} +{% block content %} +

{{ quiz.title }}

+
+ {% for q in questions %} +
+ Q{{ loop.index }}. {{ q.question_text }} + {% for key, text in {'a': q.choice_a, 'b': q.choice_b, 'c': q.choice_c, 'd': q.choice_d}.items() %} + + {% endfor %} +
+ {% else %} +

No questions yet.

+ {% endfor %} + +
+{% endblock %} \ No newline at end of file diff --git a/elearn/templates/register.html b/elearn/templates/register.html new file mode 100644 index 0000000..2510123 --- /dev/null +++ b/elearn/templates/register.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} +{% block title %}Sign up · E‑Learn{% endblock %} +{% block content %} +

Create your account

+
+ + + + +
+

Already have an account? Login.

+{% endblock %} \ No newline at end of file