From 10044b36c7e94051f0a6d42625f9df78b76af4cf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 9 Aug 2025 22:18:31 +0000 Subject: [PATCH] Initialize e-learn Flask app with user auth, courses, lessons, and quizzes Co-authored-by: egobinath2001 --- elearn/README.md | 36 ++++ elearn/__pycache__/db.cpython-313.pyc | Bin 0 -> 12495 bytes elearn/app.py | 247 +++++++++++++++++++++++ elearn/db.py | 275 ++++++++++++++++++++++++++ elearn/elearn.db | Bin 0 -> 53248 bytes elearn/requirements.txt | 1 + elearn/schema.sql | 77 ++++++++ elearn/static/app.js | 2 + elearn/static/style.css | 32 +++ elearn/templates/admin.html | 88 +++++++++ elearn/templates/base.html | 45 +++++ elearn/templates/course_detail.html | 30 +++ elearn/templates/courses.html | 16 ++ elearn/templates/dashboard.html | 20 ++ elearn/templates/index.html | 26 +++ elearn/templates/lesson_detail.html | 34 ++++ elearn/templates/login.html | 15 ++ elearn/templates/quiz.html | 21 ++ elearn/templates/register.html | 18 ++ 19 files changed, 983 insertions(+) create mode 100644 elearn/README.md create mode 100644 elearn/__pycache__/db.cpython-313.pyc create mode 100644 elearn/app.py create mode 100644 elearn/db.py create mode 100644 elearn/elearn.db create mode 100644 elearn/requirements.txt create mode 100644 elearn/schema.sql create mode 100644 elearn/static/app.js create mode 100644 elearn/static/style.css create mode 100644 elearn/templates/admin.html create mode 100644 elearn/templates/base.html create mode 100644 elearn/templates/course_detail.html create mode 100644 elearn/templates/courses.html create mode 100644 elearn/templates/dashboard.html create mode 100644 elearn/templates/index.html create mode 100644 elearn/templates/lesson_detail.html create mode 100644 elearn/templates/login.html create mode 100644 elearn/templates/quiz.html create mode 100644 elearn/templates/register.html 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 0000000000000000000000000000000000000000..fbe8eac3d700a47875c845cae52ecd5ba95e2826 GIT binary patch literal 12495 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e26f12185c6957a3884a079a80194036bf63d6cd GIT binary patch literal 53248 zcmeI)&u-f`9Kdm_P3*)?+8zezPy{8AgKach7YsY>Fsz=dbVeN~^&f0K35v`u#%wE* z&xFY3a%<|nsov99aJ(`-P+M(oKWTR@_u2XB=7axo8>`>s?*H}RpZjN< ze{TF<{CA_Z{#EghH9P;eO5lY60tjSJ;Ig<;&`Txls_k1x19>{`pPQEN%i)Rd#SS)W zEu-2oM5p>~!w|6~Q7#J6?}>V|W4th0;;>cUueM%`m&R*R?RF08O?BR`?KV21 z)@`*^jm%j}Q2yRQ%c#F-h7~VI6|IPtv1h2fHKQF?-YfTe6>-p1w`v#)q*iU$s?Uvu z>!ZfRt`E+g%Y-`=E0>?#E9j3OYgcDcFr?@AoskMew6k$72+`@K0`NZMX+fqh-zYWX z2Nj#S_}Irz-T?`qq|q^=a6u6W964RW+Gz*JJIPo?WV8?;Gl~D&IJ8ZD)8gkiP6K zF?hSp`m3%n9kx4SR>EQ*_}HQS?$+~@!w0;duIKgImX_;}dh(5TI#AzQO>68s;juXd zHg~6c#fx#ipl@$$SG!?=7N)s`TZ@$Kj9pJAbiySAG=bfo8VIEuF@nW)?iLPBV>mnu3ANN$BD+M=SR<&{`uYav44ZX1G=5DyT ztG;Rn>-ot7|6%sNcvE8p5I_I{1Q0*~0R#|0009ILNH0(fva5I_I{1Q0*~0R#|0AiV(V|MXVmL;wK<5I_I{1Q0*~ z0R#|0AgcoW|G%t8j(Q`200IagfB*srAbspan{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