forked from Ugric/Komodo-Hub
improved about page, added working class creation/join, assignment creation/completion
This commit is contained in:
1 parent
ed5147fafe
commit
86be776bcc
15 files changed
+1128
-22
No files matched your search
+176
-1
@@ -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
@@ -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()
|
||||
@@ -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():
|
||||
|
||||
@@ -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
@@ -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 — a not-for-profit organisation — 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 }} · {{ 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 %}
|
||||
@@ -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 }} · {{ 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 %}
|
||||
@@ -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 }} · {{ 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:
|
||||
<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 %}
|
||||
@@ -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:
|
||||
<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 %}
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user