improved about page, added working class creation/join, assignment creation/completion

This commit is contained in:
TommyHEC committed 2026-03-19 15:19:04 +00:00
1 parent ed5147fafe
commit 86be776bcc
15 files changed
+1128 -22

No files matched your search

+176 -1
View File
@@ -152,4 +152,179 @@ def update_species(species_id, english, latin, body, category, extinction_risk):
category,
extinction_risk,
species_id,
))
))
# ── classes ────────────────────────────────────────────────────────────────────
def _generate_class_code():
return secrets.token_hex(3).upper()
def create_class(teacher_id, name, description=""):
with get_db() as conn:
code = _generate_class_code()
while conn.execute("SELECT class_id FROM classes WHERE join_code = ?", (code,)).fetchone():
code = _generate_class_code()
conn.execute("""
INSERT INTO classes (teacher_id, name, description, join_code, created_at)
VALUES (?, ?, ?, ?, ?)
""", (teacher_id, name, description, code, datetime.now()))
def get_classes_for_teacher(teacher_id):
with get_db() as conn:
rows = conn.execute(
"SELECT * FROM classes WHERE teacher_id = ? ORDER BY created_at DESC",
(teacher_id,)
).fetchall()
return [dict(r) for r in rows]
def get_class_by_code(code):
with get_db() as conn:
row = conn.execute(
"SELECT * FROM classes WHERE join_code = ? LIMIT 1",
(code.upper(),)
).fetchone()
return dict(row) if row else None
def get_class_by_id(class_id):
with get_db() as conn:
row = conn.execute(
"SELECT * FROM classes WHERE class_id = ? LIMIT 1",
(class_id,)
).fetchone()
return dict(row) if row else None
def join_class(class_id, student_id):
with get_db() as conn:
conn.execute("""
INSERT OR IGNORE INTO class_enrolments (class_id, student_id, enrolled_at)
VALUES (?, ?, ?)
""", (class_id, student_id, datetime.now()))
def is_enrolled(class_id, student_id):
with get_db() as conn:
row = conn.execute(
"SELECT 1 FROM class_enrolments WHERE class_id = ? AND student_id = ?",
(class_id, student_id)
).fetchone()
return row is not None
def get_classes_for_student(student_id):
with get_db() as conn:
rows = conn.execute("""
SELECT c.*
FROM classes c
JOIN class_enrolments e ON c.class_id = e.class_id
WHERE e.student_id = ?
ORDER BY e.enrolled_at DESC
""", (student_id,)).fetchall()
return [dict(r) for r in rows]
def get_enrolled_students(class_id):
with get_db() as conn:
rows = conn.execute("""
SELECT u.user_id, u.username, u.email, e.enrolled_at
FROM users u
JOIN class_enrolments e ON u.user_id = e.student_id
WHERE e.class_id = ?
ORDER BY e.enrolled_at ASC
""", (class_id,)).fetchall()
return [dict(r) for r in rows]
def create_assignment(class_id, title, description, max_marks=10):
with get_db() as conn:
conn.execute("""
INSERT INTO assignments (class_id, title, description, max_marks, created_at)
VALUES (?, ?, ?, ?, ?)
""", (class_id, title, description, max_marks, datetime.now()))
def get_assignments_for_class(class_id):
with get_db() as conn:
rows = conn.execute(
"SELECT * FROM assignments WHERE class_id = ? ORDER BY created_at DESC",
(class_id,)
).fetchall()
return [dict(r) for r in rows]
def get_assignment_by_id(assignment_id):
with get_db() as conn:
row = conn.execute(
"SELECT * FROM assignments WHERE assignment_id = ? LIMIT 1",
(assignment_id,)
).fetchone()
return dict(row) if row else None
def submit_answer(assignment_id, student_id, answer_text):
with get_db() as conn:
conn.execute("""
INSERT OR IGNORE INTO submissions (assignment_id, student_id, answer_text, submitted_at)
VALUES (?, ?, ?, ?)
""", (assignment_id, student_id, answer_text, datetime.now()))
def get_submission(assignment_id, student_id):
with get_db() as conn:
row = conn.execute("""
SELECT s.*, u.username
FROM submissions s
JOIN users u ON s.student_id = u.user_id
WHERE s.assignment_id = ? AND s.student_id = ?
LIMIT 1
""", (assignment_id, student_id)).fetchone()
return dict(row) if row else None
def get_submission_by_id(submission_id):
with get_db() as conn:
row = conn.execute("""
SELECT s.*, u.username
FROM submissions s
JOIN users u ON s.student_id = u.user_id
WHERE s.submission_id = ?
LIMIT 1
""", (submission_id,)).fetchone()
return dict(row) if row else None
def get_all_submissions(assignment_id):
with get_db() as conn:
rows = conn.execute("""
SELECT s.*, u.username
FROM submissions s
JOIN users u ON s.student_id = u.user_id
WHERE s.assignment_id = ?
ORDER BY s.submitted_at ASC
""", (assignment_id,)).fetchall()
return [dict(r) for r in rows]
def get_student_submissions_for_class(class_id, student_id):
with get_db() as conn:
rows = conn.execute("""
SELECT s.*
FROM submissions s
JOIN assignments a ON s.assignment_id = a.assignment_id
WHERE a.class_id = ? AND s.student_id = ?
""", (class_id, student_id)).fetchall()
return [dict(r) for r in rows]
def mark_submission(submission_id, marks, feedback=""):
with get_db() as conn:
conn.execute("""
UPDATE submissions
SET marks = ?, feedback = ?, marked_at = ?
WHERE submission_id = ?
""", (marks, feedback, datetime.now(), submission_id))
+48 -1
View File
@@ -152,6 +152,53 @@ def init_accounts_database():
print("Accounts database initialised.")
def init_classes_database():
with get_db() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS classes (
class_id INTEGER PRIMARY KEY AUTOINCREMENT,
teacher_id INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT DEFAULT '',
join_code TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS class_enrolments (
class_id INTEGER NOT NULL,
student_id INTEGER NOT NULL,
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (class_id, student_id)
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS assignments (
assignment_id INTEGER PRIMARY KEY AUTOINCREMENT,
class_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT DEFAULT '',
max_marks INTEGER DEFAULT 10,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS submissions (
submission_id INTEGER PRIMARY KEY AUTOINCREMENT,
assignment_id INTEGER NOT NULL,
student_id INTEGER NOT NULL,
answer_text TEXT NOT NULL,
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
marks INTEGER DEFAULT NULL,
feedback TEXT DEFAULT NULL,
marked_at TIMESTAMP DEFAULT NULL,
UNIQUE (assignment_id, student_id)
)
""")
print("Classes database tables initialised.")
if __name__ == "__main__":
init_database()
init_accounts_database()
init_accounts_database()
init_classes_database()
+1 -1
View File
@@ -34,7 +34,7 @@ def import_pages(route, app):
app = Flask(__name__, static_folder="static", static_url_path="")
app.secret_key = "extremely secret key that should be changed"
@app.before_request
def load_user():
+198
View File
@@ -0,0 +1,198 @@
from flask import render_template, Blueprint, request, redirect, url_for, g, flash, abort
from database.db_commands import (
create_class, get_classes_for_teacher, get_class_by_code,
join_class, get_classes_for_student, get_class_by_id,
get_enrolled_students, create_assignment, get_assignments_for_class,
get_assignment_by_id, submit_answer, get_submission, get_all_submissions,
mark_submission, get_student_submissions_for_class, is_enrolled,
get_submission_by_id
)
page = Blueprint("classes", __name__, url_prefix="")
@page.route("/classes", methods=["GET", "POST"], strict_slashes=False)
def classes_index():
if not g.user:
return redirect(url_for("login.login"))
user = g.user
error = None
if user["account_type"] == "teacher":
if request.method == "POST":
name = request.form.get("name", "").strip()
description = request.form.get("description", "").strip()
if not name:
error = "Class name is required."
else:
create_class(user["user_id"], name, description)
flash("Class created successfully.", "info")
return redirect(url_for("classes.classes_index"))
classes = get_classes_for_teacher(user["user_id"])
return render_template("classes/teacher_classes.jinja", classes=classes, error=error)
else:
if request.method == "POST":
code = request.form.get("code", "").strip().upper()
cls = get_class_by_code(code)
if not cls:
error = "No class found with that code."
elif is_enrolled(cls["class_id"], user["user_id"]):
error = "You are already enrolled in that class."
else:
join_class(cls["class_id"], user["user_id"])
flash(f"Joined '{cls['name']}' successfully.", "info")
return redirect(url_for("classes.classes_index"))
classes = get_classes_for_student(user["user_id"])
return render_template("classes/student_classes.jinja", classes=classes, error=error)
@page.route("/classes/<int:class_id>", strict_slashes=False)
def class_detail(class_id):
if not g.user:
return redirect(url_for("login.login"))
cls = get_class_by_id(class_id)
if not cls:
abort(404)
user = g.user
if user["account_type"] == "teacher":
if cls["teacher_id"] != user["user_id"]:
abort(403)
students = get_enrolled_students(class_id)
assignments = get_assignments_for_class(class_id)
return render_template(
"classes/teacher_class_detail.jinja",
cls=cls, students=students, assignments=assignments
)
else:
if not is_enrolled(class_id, user["user_id"]):
abort(403)
assignments = get_assignments_for_class(class_id)
my_submissions = get_student_submissions_for_class(class_id, user["user_id"])
sub_map = {s["assignment_id"]: s for s in my_submissions}
return render_template(
"classes/student_class_detail.jinja",
cls=cls, assignments=assignments, sub_map=sub_map
)
@page.route("/classes/<int:class_id>/assignments/create", methods=["GET", "POST"], strict_slashes=False)
def create_assignment_route(class_id):
if not g.user or g.user["account_type"] != "teacher":
abort(403)
cls = get_class_by_id(class_id)
if not cls or cls["teacher_id"] != g.user["user_id"]:
abort(403)
error = None
if request.method == "POST":
title = request.form.get("title", "").strip()
description = request.form.get("description", "").strip()
max_marks = request.form.get("max_marks", "10").strip()
if not title:
error = "Assignment title is required."
else:
try:
max_marks = int(max_marks)
except ValueError:
max_marks = 10
create_assignment(class_id, title, description, max_marks)
flash("Assignment created.", "info")
return redirect(url_for("classes.class_detail", class_id=class_id))
return render_template("classes/create_assignment.jinja", cls=cls, error=error)
@page.route("/classes/<int:class_id>/assignments/<int:assignment_id>", strict_slashes=False)
def assignment_detail(class_id, assignment_id):
if not g.user:
return redirect(url_for("login.login"))
cls = get_class_by_id(class_id)
assignment = get_assignment_by_id(assignment_id)
if not cls or not assignment or assignment["class_id"] != class_id:
abort(404)
user = g.user
if user["account_type"] == "teacher":
if cls["teacher_id"] != user["user_id"]:
abort(403)
submissions = get_all_submissions(assignment_id)
return render_template(
"classes/teacher_assignment_detail.jinja",
cls=cls, assignment=assignment, submissions=submissions
)
else:
if not is_enrolled(class_id, user["user_id"]):
abort(403)
submission = get_submission(assignment_id, user["user_id"])
return render_template(
"classes/student_assignment.jinja",
cls=cls, assignment=assignment, submission=submission
)
@page.route("/classes/<int:class_id>/assignments/<int:assignment_id>/submit", methods=["POST"], strict_slashes=False)
def submit_assignment(class_id, assignment_id):
if not g.user or g.user["account_type"] == "teacher":
abort(403)
if not is_enrolled(class_id, g.user["user_id"]):
abort(403)
existing = get_submission(assignment_id, g.user["user_id"])
if existing:
flash("You have already submitted this assignment.", "info")
return redirect(url_for("classes.assignment_detail", class_id=class_id, assignment_id=assignment_id))
answer = request.form.get("answer", "").strip()
if not answer:
flash("Answer cannot be empty.", "info")
return redirect(url_for("classes.assignment_detail", class_id=class_id, assignment_id=assignment_id))
submit_answer(assignment_id, g.user["user_id"], answer)
flash("Answer submitted.", "info")
return redirect(url_for("classes.assignment_detail", class_id=class_id, assignment_id=assignment_id))
@page.route("/classes/<int:class_id>/assignments/<int:assignment_id>/submissions/<int:submission_id>/mark", methods=["GET", "POST"], strict_slashes=False)
def mark_submission_route(class_id, assignment_id, submission_id):
if not g.user or g.user["account_type"] != "teacher":
abort(403)
cls = get_class_by_id(class_id)
assignment = get_assignment_by_id(assignment_id)
submission = get_submission_by_id(submission_id)
if not cls or not assignment or not submission:
abort(404)
if cls["teacher_id"] != g.user["user_id"]:
abort(403)
error = None
if request.method == "POST":
marks_raw = request.form.get("marks", "").strip()
feedback = request.form.get("feedback", "").strip()
try:
marks = int(marks_raw)
if marks < 0 or marks > assignment["max_marks"]:
raise ValueError
except ValueError:
error = f"Marks must be a whole number between 0 and {assignment['max_marks']}."
else:
mark_submission(submission_id, marks, feedback)
flash("Submission marked.", "info")
return redirect(url_for("classes.assignment_detail", class_id=class_id, assignment_id=assignment_id))
return render_template(
"classes/mark_submission.jinja",
cls=cls, assignment=assignment, submission=submission, error=error
)
+74 -18
View File
@@ -1,26 +1,82 @@
{% extends "base.jinja" %}
{% block title %}About{% endblock %}
{% block head %}
{% endblock %}
{% block head %}{% endblock %}font-size: 3.5rem;
{% block content %}
{% from 'button.jinja' import button %}
<section style="padding: 100px 20px;">
<h1>About Komodo Hub</h1>
<p>
Komodo Hub is a website deisgned to assist and educate people worldwide about conservation, with a focus on Indonesia. We aim to inform people on how to better
protect wildlife and their habitats.
</p>
<p>
Badan Pusat Statistik, the Indonesian Bureau of Statistics, has recently published data on the country’s endangered endemic species. Despite spending millions
of dollars on conservation efforts, the government might have not seen any significant breakthroughs in the population of some species. The success of Komodo
has been largely exclusive, and the world is now left with merely less than 100 Sumatran Tigers, Javan Rhinoceros, Bali Mynas, Javan Eagles, Tarsius, and
Celebes Crested Macaques, respectively. Habitat loss remains the primary cause of the problems. Other causes include habitat changes, commercial
over-exploitation, the introduction of harmful non-native species, pollution, and the spread of diseases.
</p>
<p>
In 2022 the Komodo Foundation, a not-for-profit organization, was given a substantial amount of funds to initiate the development of a revolutionary digital
platform for animal conservation in Indonesia. Komodo Hub.
{# ── Hero ── #}
<section style="padding: 60px 20px 40px; text-align: center;">
<h1 style="font-size: 3.5rem; margin-bottom: 1rem; color: var(--text);">About Komodo Hub</h1>
<p style="max-width: 700px; margin: 0 auto; font-size: 1.1rem; color: var(--text); opacity: 0.9;">
A platform designed to assist and educate people worldwide about conservation, with a focus on Indonesia.
</p>
</section>
{# ── The Conservation Crisis ── #}
<section style="padding: 20px 20px 40px;">
<div class="container-full" style="max-width: 900px;">
<h2 style="margin-bottom: 1rem; color: var(--text);">The Conservation Crisis</h2>
<p style="margin-bottom: 1rem;">
Badan Pusat Statistik, the Indonesian Bureau of Statistics, has recently published data on the country's endangered endemic species.
Despite spending millions of dollars on conservation efforts, the government has not seen significant breakthroughs in the population
of some species.
</p>
<p style="margin-bottom: 1rem;">
The world is now left with merely less than 100 individuals each of the Sumatran Tiger, Javan Rhinoceros, Bali Myna, Javan Eagle,
Tarsius, and Celebes Crested Macaque. Habitat loss remains the primary cause, alongside commercial over-exploitation, the introduction
of harmful non-native species, pollution, and the spread of disease.
</p>
<p>
In 2022, the Komodo Foundation &mdash; a not-for-profit organisation &mdash; was given substantial funding to initiate the development of a
revolutionary digital platform for animal conservation in Indonesia: <strong>Komodo Hub</strong>.
</p>
</div>
</section>
{# ── Our Mission ── #}
<section style="padding: 20px 20px 40px;">
<div class="container-full" style="max-width: 900px;">
<h2 style="margin-bottom: 1rem; color: var(--text);">Our Mission</h2>
<p style="margin-bottom: 1rem;">
Komodo Hub was built to bring together researchers, conservationists, educators, students and everyday people into one shared space.
We believe that raising awareness is the first step toward real change and that a connected, informed community is the most
powerful force for protecting the natural world. By making conservation visable and engaging, we aim to turn concern
into action, and action into impact.
</p>
</div>
</section>
{# ── What We Offer ── #}
<section style="padding: 20px 20px 40px;">
<h2 style="text-align: center; margin-bottom: 1.5rem;">What We Offer</h2>
<div class="key_features">
<div style="background: var(--primary); color: var(--background);">
<h3 style="margin-bottom: 0.75rem; color: var(--background);">Species Database</h3>
<p style="font-size: 0.95rem; color: var(--background);">Browse detailed profiles of Indonesia's endangered wildlife, with up-to-date conservation status and habitat information.</p>
</div>
<div style="background: var(--primary); color: var(--background);">
<h3 style="margin-bottom: 0.75rem; color: var(--background);">Education</h3>
<p style="font-size: 0.95rem; color: var(--background);">Join the rest of your class and complete work and programs set by your teachers.</p>
</div>
<div style="background: var(--primary); color: var(--background);">
<h3 style="margin-bottom: 0.75rem; color: var(--background);">Community</h3>
<p style="font-size: 0.95rem; color: var(--background);">Join a growing network of people united by a passion for conservation by uploading your sightings.</p>
</div>
</div>
</section>
{# ── Register Now ── #}
<section style="padding: 20px 20px 40px;">
<div class="container-full" style="max-width: 900px; text-align: center;">
<h2 style="margin-bottom: 0.75rem;">Register Now</h2>
<p style="max-width: 580px; margin: 0 auto 1.5rem; opacity: 0.9;">
Create a free account to track species, contribute to our database, and connect with the conservation community.
</p>
<div class="homepage-buttons">
{{ button(url='/accounts/user_registration', button_text='Create an Account', extra_classes='homepage-btn') }}
</div>
</div>
</section>
{% endblock %}
+1 -1
View File
@@ -31,7 +31,7 @@
</div>
{#Student#}
{% if session.account_type == "student" %}
{% if user.account_type == "student" %}
<div class="container-full" style="margin-bottom: 20px;">
<h2>Classes</h2>
<p>View the classes you have joined.</p>
+39
View File
@@ -0,0 +1,39 @@
{% extends "base.jinja" %}
{% block title %}New Assignment — {{ cls.name }}{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>New Assignment</h1>
<p style="max-width: 700px; margin: 0.5rem auto; opacity:0.8;">{{ cls.name }}</p>
</section>
<section class="form-section" style="padding-top: 10px;">
<div class="container">
<h2 style="margin-bottom: 1rem;">Assignment Details</h2>
<form method="POST" class="form">
{% if error %}
<p class="error">{{ error }}</p>
{% endif %}
<div class="form-group">
<label for="title">Title</label>
<input type="text" id="title" name="title" placeholder="e.g. Critically Endangered Species Report" required>
</div>
<div class="form-group">
<label for="description">Instructions / Description</label>
<textarea id="description" name="description" rows="6"
placeholder="Describe the task, any requirements, and what students should submit…"></textarea>
</div>
<div class="form-group">
<label for="max_marks">Maximum Marks</label>
<input type="number" id="max_marks" name="max_marks" value="10" min="1" max="1000">
</div>
<button type="submit" class="submit-button" style="margin-top: 0.75rem;">Create Assignment</button>
</form>
<div style="margin-top: 1rem;">
{{ button(url=url_for('classes.class_detail', class_id=cls.class_id), button_text='Cancel', extra_classes='homepage-btn') }}
</div>
</div>
</section>
{% endblock %}
+64
View File
@@ -0,0 +1,64 @@
{% extends "base.jinja" %}
{% block title %}Mark Submission — {{ assignment.title }}{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>Mark Submission</h1>
<p style="max-width: 700px; margin: 0.5rem auto; opacity:0.75;">
{{ assignment.title }}&ensp;·&ensp;{{ cls.name }}
</p>
</section>
<section style="padding: 0 20px 40px 20px; max-width: 900px; margin: 0 auto;">
{# Student's answer #}
<div class="container-full" style="margin-bottom: 20px;">
<h2 style="margin-bottom: 0.75rem;">
{{ submission.username }}'s Answer
<span style="font-size:0.9rem; font-weight:400; opacity:0.6; margin-left:0.5rem;">
submitted {{ submission.submitted_at[:16] }}
</span>
</h2>
<div style="
background: var(--background);
border: 1.5px solid var(--primary);
border-radius: 12px;
padding: 16px 20px;
white-space: pre-wrap;
line-height: 1.7;
font-size: 0.97rem;
">{{ submission.answer_text }}</div>
</div>
{# Mark form #}
<div class="container-full">
<h2 style="margin-bottom: 1rem;">Award a Mark</h2>
<form method="POST" class="form">
{% if error %}
<p class="error">{{ error }}</p>
{% endif %}
<div class="form-group">
<label for="marks">Marks (out of {{ assignment.max_marks }})</label>
<input type="number" id="marks" name="marks"
min="0" max="{{ assignment.max_marks }}"
value="{{ submission.marks if submission.marks is not none else '' }}"
placeholder="0 – {{ assignment.max_marks }}"
required>
</div>
<div class="form-group">
<label for="feedback">Feedback <span style="font-weight:400; opacity:0.7;">(optional)</span></label>
<textarea id="feedback" name="feedback" rows="4"
placeholder="Write any feedback for the student…">{{ submission.feedback or '' }}</textarea>
</div>
<button type="submit" class="submit-button" style="margin-top:0.5rem;">Save Mark</button>
</form>
</div>
</section>
<section style="padding: 0 20px 20px; max-width: 900px; margin: 0 auto;">
{{ button(url=url_for('classes.assignment_detail', class_id=cls.class_id, assignment_id=assignment.assignment_id), button_text='← Back to Assignment', extra_classes='homepage-btn') }}
</section>
{% endblock %}
+105
View File
@@ -0,0 +1,105 @@
{% extends "base.jinja" %}
{% block title %}{{ assignment.title }}{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>{{ assignment.title }}</h1>
<p style="max-width: 700px; margin: 0.5rem auto; opacity:0.75;">{{ cls.name }}&ensp;·&ensp;{{ assignment.max_marks }} marks</p>
</section>
<section style="padding: 0 20px 40px 20px; max-width: 900px; margin: 0 auto;">
{% if assignment.description %}
<div class="container-full" style="margin-bottom: 20px;">
<h2 style="margin-bottom: 0.75rem;">Instructions</h2>
<p style="white-space: pre-wrap; line-height: 1.7;">{{ assignment.description }}</p>
</div>
{% endif %}
{% if submission %}
{# Already submitted — show what they wrote + any mark/feedback #}
<div class="container-full" style="margin-bottom: 20px;">
<h2 style="margin-bottom: 0.75rem;">Your Submission</h2>
<div style="
background: var(--background);
border: 1.5px solid var(--primary);
border-radius: 12px;
padding: 16px 20px;
white-space: pre-wrap;
line-height: 1.7;
font-size: 0.97rem;
">{{ submission.answer_text }}</div>
<p style="opacity:0.55; font-size:0.82rem; margin-top:6px;">
Submitted {{ submission.submitted_at[:16] }}
</p>
</div>
{# Result #}
<div class="container-full">
<h2 style="margin-bottom: 0.75rem;">Result</h2>
{% if submission.marks is not none %}
<div style="display:flex; align-items:center; gap: 1.5rem; flex-wrap:wrap; margin-bottom: 1rem;">
<div style="
font-size: 3rem;
font-weight: 900;
background: linear-gradient(120deg, var(--primary), var(--accent));
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
line-height: 1;
">{{ submission.marks }}<span style="font-size:1.5rem; opacity:0.7;">/{{ assignment.max_marks }}</span></div>
<div>
{% set pct = (submission.marks / assignment.max_marks * 100) | int %}
{% if pct >= 80 %}
<span style="font-size:1.1rem; font-weight:700; color: var(--green);">Excellent work!</span>
{% elif pct >= 60 %}
<span style="font-size:1.1rem; font-weight:700; color: var(--text);">Good effort.</span>
{% elif pct >= 40 %}
<span style="font-size:1.1rem; font-weight:700; color: var(--text);">Keep working at it.</span>
{% else %}
<span style="font-size:1.1rem; font-weight:700; color: var(--red);">See your teacher for support.</span>
{% endif %}
</div>
</div>
{% if submission.feedback %}
<div style="
background: var(--background);
border-left: 4px solid var(--primary);
border-radius: 0 10px 10px 0;
padding: 12px 16px;
font-size: 0.95rem;
line-height: 1.6;
">
<strong>Teacher feedback:</strong><br>{{ submission.feedback }}
</div>
{% endif %}
{% else %}
<p style="opacity:0.7;">
Your submission is awaiting marking. Check back later.
</p>
{% endif %}
</div>
{% else %}
{# Not yet submitted — show the answer form #}
<div class="container-full">
<h2 style="margin-bottom: 0.75rem;">Your Answer</h2>
<form method="POST" action="{{ url_for('classes.submit_assignment', class_id=cls.class_id, assignment_id=assignment.assignment_id) }}" class="form">
<div class="form-group">
<label for="answer">Write your answer below</label>
<textarea id="answer" name="answer" rows="10"
placeholder="Type your answer here…" required></textarea>
</div>
<button type="submit" class="submit-button" style="margin-top: 0.5rem;">Submit Answer</button>
</form>
</div>
{% endif %}
</section>
<section style="padding: 0 20px 20px; max-width: 900px; margin: 0 auto;">
{{ button(url=url_for('classes.class_detail', class_id=cls.class_id), button_text='← Back to Class', extra_classes='homepage-btn') }}
</section>
{% endblock %}
@@ -0,0 +1,95 @@
{% extends "base.jinja" %}
{% block title %}{{ cls.name }}{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>{{ cls.name }}</h1>
{% if cls.description %}
<p style="max-width: 700px; margin: 0.5rem auto; opacity:0.8;">{{ cls.description }}</p>
{% endif %}
</section>
<section style="padding: 0 20px 40px 20px; max-width: 900px; margin: 0 auto;">
<div class="container-full">
<h2 style="margin-bottom: 1rem;">Assignments</h2>
{% if assignments %}
<div style="display: flex; flex-direction: column; gap: 12px;">
{% for a in assignments %}
{% set sub = sub_map.get(a.assignment_id) %}
<a href="{{ url_for('classes.assignment_detail', class_id=cls.class_id, assignment_id=a.assignment_id) }}"
style="
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
text-decoration: none;
color: var(--text);
background: var(--background);
border: 1.5px solid var(--primary);
border-radius: 12px;
padding: 14px 20px;
transition: border-color 0.2s;
"
onmouseover="this.style.borderColor='var(--green)'"
onmouseout="this.style.borderColor='var(--primary)'">
<div>
<span style="font-weight: 700; font-size:1.05rem;">{{ a.title }}</span>
{% if a.description %}
<p style="margin:4px 0 0; opacity:0.65; font-size:0.9rem;">{{ a.description | truncate(80) }}</p>
{% endif %}
</div>
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<span style="
background: var(--green3);
border: 1px solid var(--primary);
border-radius: 8px;
padding: 4px 12px;
font-size: 0.85rem;
">{{ a.max_marks }} marks</span>
{% if sub %}
{% if sub.marks is not none %}
<span style="
background: var(--primary_darker);
border-radius: 8px;
padding: 4px 12px;
font-size: 0.85rem;
font-weight: 700;
">{{ sub.marks }}/{{ a.max_marks }} ✓</span>
{% else %}
<span style="
background: var(--accent);
border-radius: 8px;
padding: 4px 12px;
font-size: 0.85rem;
">Submitted — awaiting mark</span>
{% endif %}
{% else %}
<span style="
background: var(--background);
border: 1px dashed var(--accent);
border-radius: 8px;
padding: 4px 12px;
font-size: 0.85rem;
opacity: 0.8;
">Not submitted</span>
{% endif %}
</div>
</a>
{% endfor %}
</div>
{% else %}
<p style="opacity:0.6;">Your teacher has not set any assignments yet.</p>
{% endif %}
</div>
</section>
<section style="padding: 0 20px 20px; max-width: 900px; margin: 0 auto;">
{{ button(url=url_for('classes.classes_index'), button_text='← Back to Classes', extra_classes='homepage-btn') }}
</section>
{% endblock %}
+55
View File
@@ -0,0 +1,55 @@
{% extends "base.jinja" %}
{% block title %}My Classes{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>My Classes</h1>
<p style="max-width: 700px; margin: 0.5rem auto;">Enter a join code from your teacher to enrol in a class.</p>
</section>
<section style="padding: 0 20px 40px 20px; max-width: 900px; margin: 0 auto;">
{# Join class form #}
<div class="container-full" style="margin-bottom: 20px;">
<h2 style="margin-bottom: 1rem;">Join a Class</h2>
<form method="POST" class="form">
{% if error %}
<p class="error">{{ error }}</p>
{% endif %}
<div class="form-group">
<label for="code">Class Join Code</label>
<input type="text" id="code" name="code"
placeholder="e.g. A3F9C2"
maxlength="6"
style="text-transform: uppercase; letter-spacing: 0.15em; font-family: monospace; font-size: 1.2rem;"
required>
</div>
<button type="submit" class="submit-button" style="margin-top: 0.5rem;">Join Class</button>
</form>
</div>
{# Enrolled classes #}
<div class="container-full">
<h2 style="margin-bottom: 1rem;">Enrolled Classes</h2>
{% if classes %}
<div class="species-grid">
{% for cls in classes %}
<a href="{{ url_for('classes.class_detail', class_id=cls.class_id) }}"
class="container-lite"
style="display:block; text-decoration:none; color: var(--text);">
<h3 style="margin-bottom: 0.4rem;">{{ cls.name }}</h3>
{% if cls.description %}
<p style="opacity:0.75; margin-bottom: 0.5rem; font-size:0.95rem;">{{ cls.description }}</p>
{% endif %}
<p style="opacity:0.6; font-size:0.85rem; margin:0;">View assignments →</p>
</a>
{% endfor %}
</div>
{% else %}
<p style="opacity:0.6;">You have not joined any classes yet. Use the form above to join with your teacher's code.</p>
{% endif %}
</div>
</section>
{% endblock %}
@@ -0,0 +1,92 @@
{% extends "base.jinja" %}
{% block title %}{{ assignment.title }} — {{ cls.name }}{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>{{ assignment.title }}</h1>
<p style="max-width: 700px; margin: 0.5rem auto; opacity:0.75;">{{ cls.name }}&ensp;·&ensp;{{ assignment.max_marks }} marks</p>
</section>
<section style="padding: 0 20px 40px 20px; max-width: 900px; margin: 0 auto;">
{% if assignment.description %}
<div class="container-full" style="margin-bottom: 20px;">
<h2 style="margin-bottom: 0.75rem;">Instructions</h2>
<p style="white-space: pre-wrap; line-height: 1.7;">{{ assignment.description }}</p>
</div>
{% endif %}
<div class="container-full">
<h2 style="margin-bottom: 1rem;">Submissions
<span style="font-size:1rem; font-weight:400; opacity:0.7;">({{ submissions | length }})</span>
</h2>
{% if submissions %}
<div style="display: flex; flex-direction: column; gap: 14px;">
{% for sub in submissions %}
<div style="
background: var(--background);
border: 1.5px solid {% if sub.marks is not none %}var(--primary){% else %}var(--accent){% endif %};
border-radius: 14px;
padding: 18px 22px;
">
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px; margin-bottom:10px;">
<span style="font-weight:700; font-size:1.05rem;">{{ sub.username }}</span>
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<span style="opacity:0.55; font-size:0.8rem;">{{ sub.submitted_at[:16] }}</span>
{% if sub.marks is not none %}
<span style="
background: var(--primary_darker);
border-radius: 8px;
padding: 3px 12px;
font-size: 0.9rem;
font-weight: 700;
">{{ sub.marks }} / {{ assignment.max_marks }}</span>
{% else %}
<span style="
background: var(--accent);
border-radius: 8px;
padding: 3px 12px;
font-size: 0.85rem;
">Ungraded</span>
{% endif %}
</div>
</div>
<div style="
background: var(--green3);
border-radius: 10px;
padding: 12px 16px;
font-size: 0.95rem;
white-space: pre-wrap;
line-height: 1.6;
margin-bottom: 10px;
">{{ sub.answer_text }}</div>
{% if sub.feedback %}
<div style="opacity:0.75; font-size:0.9rem; margin-bottom: 10px;">
<strong>Feedback:</strong> {{ sub.feedback }}
</div>
{% endif %}
<a href="{{ url_for('classes.mark_submission_route', class_id=cls.class_id, assignment_id=assignment.assignment_id, submission_id=sub.submission_id) }}"
class="link_button"
style="display:inline-block; padding: 8px 18px; font-size:0.95rem; margin: 0;">
{% if sub.marks is not none %}Edit Mark{% else %}Mark{% endif %}
</a>
</div>
{% endfor %}
</div>
{% else %}
<p style="opacity:0.6;">No students have submitted yet.</p>
{% endif %}
</div>
</section>
<section style="padding: 0 20px 20px; max-width: 900px; margin: 0 auto;">
{{ button(url=url_for('classes.class_detail', class_id=cls.class_id), button_text='← Back to Class', extra_classes='homepage-btn') }}
</section>
{% endblock %}
@@ -0,0 +1,114 @@
{% extends "base.jinja" %}
{% block title %}{{ cls.name }}{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>{{ cls.name }}</h1>
{% if cls.description %}
<p style="max-width: 700px; margin: 0.5rem auto; opacity:0.8;">{{ cls.description }}</p>
{% endif %}
<p style="margin-top: 0.75rem;">
Join code:&nbsp;
<span style="
font-family: monospace;
font-size: 1.4rem;
font-weight: 700;
letter-spacing: 0.18em;
background: var(--green3);
border: 2px solid var(--primary);
border-radius: 10px;
padding: 4px 16px;
">{{ cls.join_code }}</span>
</p>
</section>
<section style="padding: 0 20px 40px 20px; max-width: 900px; margin: 0 auto;">
{# Enrolled students #}
<div class="container-full" style="margin-bottom: 20px;">
<h2 style="margin-bottom: 1rem;">Enrolled Students
<span style="font-size:1rem; font-weight:400; opacity:0.7;">({{ students | length }})</span>
</h2>
{% if students %}
<div style="display: flex; flex-direction: column; gap: 10px;">
{% for student in students %}
<div style="
display: flex;
align-items: center;
justify-content: space-between;
background: var(--background);
border: 1.5px solid var(--primary);
border-radius: 12px;
padding: 12px 20px;
">
<div>
<span style="font-weight: 700;">{{ student.username }}</span>
<span style="opacity:0.6; margin-left: 0.75rem; font-size:0.9rem;">{{ student.email }}</span>
</div>
<span style="opacity:0.5; font-size:0.8rem;">
Joined {{ student.enrolled_at[:10] }}
</span>
</div>
{% endfor %}
</div>
{% else %}
<p style="opacity:0.6;">No students have joined yet. Share the join code above with your class.</p>
{% endif %}
</div>
{# Assignments #}
<div class="container-full">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; flex-wrap:wrap; gap:10px;">
<h2>Assignments</h2>
{{ button(url=url_for('classes.create_assignment_route', class_id=cls.class_id), button_text='+ New Assignment', extra_classes='homepage-btn') }}
</div>
{% if assignments %}
<div style="display: flex; flex-direction: column; gap: 12px;">
{% for a in assignments %}
<a href="{{ url_for('classes.assignment_detail', class_id=cls.class_id, assignment_id=a.assignment_id) }}"
style="
display: flex;
align-items: center;
justify-content: space-between;
text-decoration: none;
color: var(--text);
background: var(--background);
border: 1.5px solid var(--primary);
border-radius: 12px;
padding: 14px 20px;
transition: border-color 0.2s;
"
onmouseover="this.style.borderColor='var(--green)'"
onmouseout="this.style.borderColor='var(--primary)'">
<div>
<span style="font-weight: 700; font-size:1.05rem;">{{ a.title }}</span>
{% if a.description %}
<p style="margin:4px 0 0; opacity:0.65; font-size:0.9rem;">{{ a.description | truncate(80) }}</p>
{% endif %}
</div>
<span style="
white-space: nowrap;
background: var(--green3);
border: 1px solid var(--primary);
border-radius: 8px;
padding: 4px 12px;
font-size: 0.85rem;
margin-left: 1rem;
">{{ a.max_marks }} marks</span>
</a>
{% endfor %}
</div>
{% else %}
<p style="opacity:0.6;">No assignments yet. Create one using the button above.</p>
{% endif %}
</div>
</section>
<section style="padding: 0 20px 20px; max-width: 900px; margin: 0 auto;">
{{ button(url=url_for('classes.classes_index'), button_text='← Back to Classes', extra_classes='homepage-btn') }}
</section>
{% endblock %}
+65
View File
@@ -0,0 +1,65 @@
{% extends "base.jinja" %}
{% block title %}My Classes{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>My Classes</h1>
<p style="max-width: 700px; margin: 0.5rem auto;">Create and manage your classes, then share the join code with your students.</p>
</section>
<section style="padding: 0 20px 40px 20px; max-width: 900px; margin: 0 auto;">
{# Create class form #}
<div class="container-full" style="margin-bottom: 20px;">
<h2 style="margin-bottom: 1rem;">Create a New Class</h2>
<form method="POST" class="form">
{% if error %}
<p class="error">{{ error }}</p>
{% endif %}
<div class="form-group">
<label for="name">Class Name</label>
<input type="text" id="name" name="name" placeholder="e.g. Year 10 Biology" required>
</div>
<div class="form-group">
<label for="description">Description <span style="font-weight:400; opacity:0.7;">(optional)</span></label>
<textarea id="description" name="description" rows="3" placeholder="A short description of this class…"></textarea>
</div>
<button type="submit" class="submit-button" style="margin-top: 0.5rem;">Create Class</button>
</form>
</div>
{# Existing classes #}
<div class="container-full">
<h2 style="margin-bottom: 1rem;">Your Classes</h2>
{% if classes %}
<div class="species-grid">
{% for cls in classes %}
<a href="{{ url_for('classes.class_detail', class_id=cls.class_id) }}" class="container-lite" style="display:block; text-decoration:none; color: var(--text);">
<h3 style="margin-bottom: 0.4rem;">{{ cls.name }}</h3>
{% if cls.description %}
<p style="opacity:0.75; margin-bottom: 0.8rem; font-size:0.95rem;">{{ cls.description }}</p>
{% endif %}
<p style="margin-bottom: 0.4rem; font-size: 0.9rem;">
Join code:&nbsp;
<span style="
font-family: monospace;
font-size: 1.15rem;
font-weight: 700;
letter-spacing: 0.15em;
background: var(--background);
border: 1.5px solid var(--primary);
border-radius: 8px;
padding: 2px 10px;
">{{ cls.join_code }}</span>
</p>
</a>
{% endfor %}
</div>
{% else %}
<p style="opacity:0.6;">You have not created any classes yet. Use the form above to get started.</p>
{% endif %}
</div>
</section>
{% endblock %}
+1
View File
@@ -12,6 +12,7 @@
<div class="dropdown">
<button class="dropbtn">Devtools ▾</button>
<div class="dropdown-content">
<a href="/about">About</a>
<a href="/species/portal">Add Species</a>
<a href="/species">View Species</a>
<a href="/species_search">Search Species</a>