forked from Ugric/Komodo-Hub
merge upstream
This commit is contained in:
commit
739199a0b7
21 files changed
+1610
-47
No files matched your search
+251
-1
@@ -152,4 +152,254 @@ 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))
|
||||
|
||||
|
||||
# ── programs ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def create_program(leader_id, title, description=""):
|
||||
with get_db() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO programs (leader_id, title, description, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (leader_id, title, description, datetime.now()))
|
||||
|
||||
|
||||
def get_all_programs():
|
||||
with get_db() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM programs ORDER BY created_at DESC"
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def get_program_by_id(program_id):
|
||||
with get_db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM programs WHERE program_id = ? LIMIT 1",
|
||||
(program_id,)
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def get_programs_by_leader(leader_id):
|
||||
with get_db() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM programs WHERE leader_id = ? ORDER BY created_at DESC",
|
||||
(leader_id,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def join_program(program_id, user_id):
|
||||
with get_db() as conn:
|
||||
conn.execute("""
|
||||
INSERT OR IGNORE INTO program_enrolments (program_id, user_id, enrolled_at)
|
||||
VALUES (?, ?, ?)
|
||||
""", (program_id, user_id, datetime.now()))
|
||||
|
||||
|
||||
def is_enrolled_in_program(program_id, user_id):
|
||||
with get_db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM program_enrolments WHERE program_id = ? AND user_id = ?",
|
||||
(program_id, user_id)
|
||||
).fetchone()
|
||||
return row is not None
|
||||
|
||||
|
||||
def get_programs_for_user(user_id):
|
||||
with get_db() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT p.*
|
||||
FROM programs p
|
||||
JOIN program_enrolments e ON p.program_id = e.program_id
|
||||
WHERE e.user_id = ?
|
||||
ORDER BY e.enrolled_at DESC
|
||||
""", (user_id,)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def search_programs(query):
|
||||
with get_db() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT * FROM programs
|
||||
WHERE instr(LOWER(COALESCE(CAST(title AS TEXT), '')), ?1) > 0
|
||||
OR instr(LOWER(COALESCE(CAST(description AS TEXT), '')), ?1) > 0
|
||||
""", (query.lower(),)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
+75
-4
@@ -11,7 +11,7 @@ SEED_SPECIES = [
|
||||
"Its dense forest habitat is rapidly disappearing due to agricultural expansion and "
|
||||
"illegal logging. Poaching for the illegal wildlife trade remains a severe threat. "
|
||||
"Conservation efforts include protected national parks, anti-poaching patrols, and "
|
||||
"community engagement programmes."
|
||||
"community engagement programs."
|
||||
),
|
||||
"category": "Mammal",
|
||||
"extinction_risk": "Critically Endangered",
|
||||
@@ -38,7 +38,7 @@ SEED_SPECIES = [
|
||||
"The Bali Myna is a striking white bird endemic to the island of Bali. Fewer than "
|
||||
"100 individuals survive in the wild, making it one of the rarest birds in the world. "
|
||||
"The primary drivers of its decline are the illegal pet trade and loss of its lowland "
|
||||
"forest habitat. Captive breeding and reintroduction programmes have been key "
|
||||
"forest habitat. Captive breeding and reintroduction programs have been key "
|
||||
"strategies in efforts to stabilise and grow its wild population."
|
||||
),
|
||||
"category": "Bird",
|
||||
@@ -82,7 +82,7 @@ SEED_SPECIES = [
|
||||
"The Celebes Crested Macaque is endemic to north-east Sulawesi, recognised by "
|
||||
"its jet-black coat and distinctive crest. Fewer than 5,500 individuals are thought "
|
||||
"to survive. The species is hunted for bushmeat and its forest habitat continues "
|
||||
"to shrink. Conservation programmes focus on community education, law enforcement, "
|
||||
"to shrink. Conservation programs focus on community education, law enforcement, "
|
||||
"and ecotourism as a sustainable alternative income source for local communities."
|
||||
),
|
||||
"category": "Mammal",
|
||||
@@ -108,6 +108,7 @@ def init_database():
|
||||
""")
|
||||
print("Database initialised.")
|
||||
seed_species()
|
||||
init_programs_database()
|
||||
|
||||
|
||||
def seed_species():
|
||||
@@ -152,6 +153,76 @@ 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.")
|
||||
|
||||
|
||||
def init_programs_database():
|
||||
with get_db() as conn:
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS programs (
|
||||
program_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
leader_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS program_enrolments (
|
||||
program_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (program_id, user_id)
|
||||
)
|
||||
""")
|
||||
print("Programs database tables initialised.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_database()
|
||||
init_accounts_database()
|
||||
init_accounts_database()
|
||||
init_classes_database()
|
||||
init_programs_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
|
||||
)
|
||||
@@ -0,0 +1,127 @@
|
||||
from flask import render_template, Blueprint, request, redirect, url_for, flash, g
|
||||
from database.db_commands import (
|
||||
create_program,
|
||||
get_all_programs,
|
||||
get_program_by_id,
|
||||
get_programs_for_user,
|
||||
join_program,
|
||||
is_enrolled_in_program,
|
||||
search_programs,
|
||||
get_programs_by_leader,
|
||||
)
|
||||
|
||||
page = Blueprint("programs", __name__, url_prefix="/programs")
|
||||
|
||||
|
||||
def require_login():
|
||||
if not g.user:
|
||||
flash("You must be logged in to access that page.", "info")
|
||||
return redirect(url_for("home.home"))
|
||||
return None
|
||||
|
||||
@page.route("/")
|
||||
def programs_index():
|
||||
redirect_response = require_login()
|
||||
if redirect_response:
|
||||
return redirect_response
|
||||
|
||||
query = request.args.get("search-query", "").strip()
|
||||
|
||||
if query:
|
||||
results = search_programs(query)
|
||||
else:
|
||||
results = get_all_programs()
|
||||
user_id = g.user["user_id"]
|
||||
for prog in results:
|
||||
prog["enrolled"] = is_enrolled_in_program(prog["program_id"], user_id)
|
||||
|
||||
return render_template(
|
||||
"programs/programs.jinja",
|
||||
programs=results,
|
||||
query=query,
|
||||
)
|
||||
|
||||
@page.route("/create", methods=["GET", "POST"])
|
||||
def create():
|
||||
redirect_response = require_login()
|
||||
if redirect_response:
|
||||
return redirect_response
|
||||
|
||||
if g.user["account_type"] != "community_leader":
|
||||
flash("Only community leaders can create programs.", "info")
|
||||
return redirect(url_for("programs.programs_index"))
|
||||
|
||||
if request.method == "POST":
|
||||
title = request.form.get("title", "").strip()
|
||||
description = request.form.get("description", "").strip()
|
||||
|
||||
if not title:
|
||||
flash("A program title is required.", "info")
|
||||
return render_template("programs/create.jinja")
|
||||
|
||||
create_program(
|
||||
leader_id=g.user["user_id"],
|
||||
title=title,
|
||||
description=description,
|
||||
)
|
||||
flash("Program created successfully.", "info")
|
||||
return redirect(url_for("programs.my_programs"))
|
||||
|
||||
return render_template("programs/create.jinja")
|
||||
|
||||
|
||||
@page.route("/my")
|
||||
def my_programs():
|
||||
redirect_response = require_login()
|
||||
if redirect_response:
|
||||
return redirect_response
|
||||
|
||||
user_id = g.user["user_id"]
|
||||
|
||||
if g.user["account_type"] == "community_leader":
|
||||
programs = get_programs_by_leader(user_id)
|
||||
else:
|
||||
programs = get_programs_for_user(user_id)
|
||||
|
||||
return render_template(
|
||||
"programs/my_programs.jinja",
|
||||
programs=programs,
|
||||
)
|
||||
|
||||
@page.route("/<int:program_id>")
|
||||
def view_program(program_id):
|
||||
redirect_response = require_login()
|
||||
if redirect_response:
|
||||
return redirect_response
|
||||
|
||||
program = get_program_by_id(program_id)
|
||||
if not program:
|
||||
flash("Program not found.", "info")
|
||||
return redirect(url_for("programs.programs_index"))
|
||||
|
||||
enrolled = is_enrolled_in_program(program_id, g.user["user_id"])
|
||||
|
||||
return render_template(
|
||||
"programs/view_program.jinja",
|
||||
program=program,
|
||||
enrolled=enrolled,
|
||||
)
|
||||
|
||||
@page.route("/<int:program_id>/join", methods=["POST"])
|
||||
def join(program_id):
|
||||
redirect_response = require_login()
|
||||
if redirect_response:
|
||||
return redirect_response
|
||||
|
||||
program = get_program_by_id(program_id)
|
||||
if not program:
|
||||
flash("Program not found.", "info")
|
||||
return redirect(url_for("programs.programs_index"))
|
||||
|
||||
if is_enrolled_in_program(program_id, g.user["user_id"]):
|
||||
flash("You are already enrolled in this program.", "info")
|
||||
return redirect(url_for("programs.view_program", program_id=program_id))
|
||||
|
||||
join_program(program_id, g.user["user_id"])
|
||||
flash(f"You have joined \"{program['title']}\".", "info")
|
||||
return redirect(url_for("programs.view_program", program_id=program_id))
|
||||
+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 %}
|
||||
@@ -11,31 +11,26 @@
|
||||
|
||||
<section style="padding: 0 20px 40px 20px; max-width: 900px; margin: 0 auto;">
|
||||
|
||||
{#Enrolled Programmes (everyone sees this)#}
|
||||
<div class="container-full" style="margin-bottom: 20px;">
|
||||
<h2>Your Programmes</h2>
|
||||
{% if enrolled_programmes %}
|
||||
<div class="species-grid">
|
||||
{% for prog in enrolled_programmes %}
|
||||
<div class="container-lite">
|
||||
<h3>{{ prog.name }}</h3>
|
||||
<p>{{ prog.description }}</p>
|
||||
{{ button(url='/programmes/' ~ prog.id, button_text='View', extra_classes='homepage-btn') }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p style="color: var(--text); opacity: 0.6;">You are not enrolled in any programmes yet.</p>
|
||||
{{ button(url='/programmes', button_text='Browse Programmes', extra_classes='homepage-btn') }}
|
||||
{% endif %}
|
||||
{#Enrolled Programs (everyone except community leaders sees this)#}
|
||||
{% if user.account_type != "community_leader" %}
|
||||
<div class="container-full" style="margin-bottom: 20px">
|
||||
<h2>Programs</h2>
|
||||
<p>Browse and join programs here</p>
|
||||
<div class="homepage-buttons" style="justify-content: flex-start; margin-top: 1rem;">
|
||||
{{ button(url='/programs', button_text='Browse Programs', extra_classes='homepage-btn') }}
|
||||
{{ button(url='/programs/my', button_text='Joined Programs', extra_classes='homepage-btn') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{#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>
|
||||
{{ button(url='/classes', button_text='View Classes', extra_classes='homepage-btn') }}
|
||||
<div class="homepage-buttons" style="justify-content: flex-start; margin-top: 1rem;">
|
||||
{{ button(url='/classes', button_text='View Classes', extra_classes='homepage-btn') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -53,9 +48,12 @@
|
||||
{#Community Leader#}
|
||||
{% if user.account_type == "community_leader" %}
|
||||
<div class="container-full" style="margin-bottom: 20px;">
|
||||
<h2>Programmes</h2>
|
||||
<p>Create a new conservation programme for others to join.</p>
|
||||
{{ button(url='/programmes/create', button_text='Create a Programme', extra_classes='homepage-btn') }}
|
||||
<h2>Programs</h2>
|
||||
<p>Manage your conservation programs or create a new one.</p>
|
||||
<div class="homepage-buttons" style="justify-content: flex-start; margin-top: 1rem;">
|
||||
{{ button(url='/programs/my', button_text='My Programs', extra_classes='homepage-btn') }}
|
||||
{{ button(url='/programs/create', button_text='Create a Program', extra_classes='homepage-btn') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{% extends "base.jinja" %}
|
||||
{% block title %}Create a Program{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<section id="hero" style="padding: 40px 20px; text-align: center;">
|
||||
<h1>Create a Program</h1>
|
||||
<p style="max-width: 700px; margin: 0.5rem auto;">
|
||||
Set up a new conservation program for others to discover and join.
|
||||
</p>
|
||||
</section>
|
||||
<section class="form-section">
|
||||
<div class="container">
|
||||
<h2 style="margin-bottom: 1rem;">Program Details</h2>
|
||||
<form action="/programs/create" method="POST" class="form">
|
||||
<div class="form-group">
|
||||
<label for="title">Program Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
placeholder="e.g. Sumatran Tiger Patrol"
|
||||
required
|
||||
maxlength="120"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
rows="6"
|
||||
placeholder="Describe the goals, activities, and impact of your program..."
|
||||
maxlength="2000"
|
||||
></textarea>
|
||||
</div>
|
||||
<button type="submit" class="submit-button" style="margin-top: 10px;">
|
||||
Create Program
|
||||
</button>
|
||||
</form>
|
||||
<div style="text-align: center; margin-top: 1rem;">
|
||||
<a href="/programs/my" style="opacity: 0.7; font-size: 0.9rem;">
|
||||
← Back to my programs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "base.jinja" %}
|
||||
{% block title %}My Programs{% endblock %}
|
||||
{% block content %}
|
||||
{% from 'button.jinja' import button %}
|
||||
|
||||
<section id="hero" style="padding: 40px 20px; text-align: center;">
|
||||
{% if user.account_type == 'community_leader' %}
|
||||
<h1>Your Programs</h1>
|
||||
<p style="max-width: 700px; margin: 0.5rem auto;">
|
||||
Programs you have created and are running.
|
||||
</p>
|
||||
{% else %}
|
||||
<h1>My Programs</h1>
|
||||
<p style="max-width: 700px; margin: 0.5rem auto;">
|
||||
Conservation programs you are enrolled in.
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section style="padding: 0 20px 60px 20px; max-width: 960px; margin: 0 auto;">
|
||||
|
||||
{% if programs %}
|
||||
<div class="container-full">
|
||||
<h2>Your programs</h2>
|
||||
{% for prog in programs %}
|
||||
<div class="container-lite" style="display: flex; flex-direction: column; gap: 10px; padding: 20px; margin-top: 20px">
|
||||
<h3 style="margin-bottom: 4px;">{{ prog.title }}</h3>
|
||||
<p style="opacity: 0.8; flex: 1;">
|
||||
{{ prog.description if prog.description else 'No description provided.' }}
|
||||
</p>
|
||||
<div style="margin-top: 8px;">
|
||||
<div class="homepage-buttons" style="justify-content: flex-start; margin-top: 1rem;">
|
||||
{{ button(url='/programs/' ~ prog.program_id, button_text='View', extra_classes='homepage-btn') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="container-full" style="text-align: center;">
|
||||
{% if user.account_type == 'community_leader' %}
|
||||
<p style="opacity: 0.7;">You have not created any programs yet.</p>
|
||||
<div class="homepage-buttons" style="justify-content: flex-start; margin-top: 1rem;">
|
||||
{{ button(url='/programs/create', button_text='Create a Program', extra_classes='homepage-btn') }}
|
||||
</div>
|
||||
{% else %}
|
||||
<p style="opacity: 0.7;">You are not enrolled in any programs yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Action bar at the bottom #}
|
||||
<div style="margin-top: 24px; display: flex; gap: 12px; flex-wrap: wrap; justify-content: center;">
|
||||
{% if user.account_type == 'community_leader' %}
|
||||
<div class="homepage-buttons" style="justify-content: flex-start; margin-top: 1rem;">
|
||||
{{ button(url='/programs/create', button_text='+ Create New Program', extra_classes='homepage-btn') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="homepage-buttons" style="justify-content: flex-start; margin-top: 1rem;">
|
||||
{{ button(url='/programs', button_text='Browse All Programs', extra_classes='homepage-btn') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,84 @@
|
||||
{% extends "base.jinja" %}
|
||||
{% block title %}Programs{% endblock %}
|
||||
{% block content %}
|
||||
{% from 'button.jinja' import button %}
|
||||
{% from 'search_bar.jinja' import search_bar %}
|
||||
|
||||
<section id="hero" style="padding: 40px 20px; text-align: center;">
|
||||
<h1>Conservation Programs</h1>
|
||||
<p style="max-width: 700px; margin: 0.5rem auto;">
|
||||
Discover and join community-led conservation programs, or search for one that matches your interests.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style="padding: 0 20px 60px 20px; max-width: 960px; margin: 0 auto;">
|
||||
|
||||
{# Search bar #}
|
||||
<div class="container-full" style="margin-bottom: 20px;">
|
||||
<h2>Search programs...</h2>
|
||||
{{ search_bar('/programs', query) }}
|
||||
|
||||
{% if query %}
|
||||
<p style="text-align: center; opacity: 0.7; margin-top: -1rem;">
|
||||
{{ programs | length }} result{{ 's' if programs | length != 1 }} for
|
||||
<strong>"{{ query }}"</strong>
|
||||
— <a href="/programs">clear</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Results grid #}
|
||||
{% if programs %}
|
||||
<div class="container-full">
|
||||
<h2>Programs...</h2>
|
||||
{% for prog in programs %}
|
||||
<div class="container-lite" style="display: flex; flex-direction: column; gap: 10px; padding: 20px; margin-top: 20px">
|
||||
<h3 style="margin-bottom: 4px;">{{ prog.title }}</h3>
|
||||
<p style="opacity: 0.8; flex: 1;">
|
||||
{{ prog.description if prog.description else 'No description provided.' }}
|
||||
</p>
|
||||
<div style="height: 10px;"></div>
|
||||
<div class="button">
|
||||
{{ button(url='/programs/' ~ prog.program_id, button_text='View', extra_classes='homepage-btn') }}
|
||||
{% if prog.enrolled %}
|
||||
<span class="link_button homepage-btn"
|
||||
style="background-color: var(--accent); cursor: default; text-align: center;">
|
||||
✓ Enrolled
|
||||
</span>
|
||||
<div style="height: 10px;"></div>
|
||||
{% else %}
|
||||
<form action="/programs/{{ prog.program_id }}/join" method="POST" style="display:inline;">
|
||||
<button type="submit" class="link_button homepage-btn"
|
||||
style="border: none; cursor: pointer;">
|
||||
Join
|
||||
</button>
|
||||
</form>
|
||||
<div style="height: 10px;"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="container-full" style="text-align: center;">
|
||||
<p style="opacity: 0.7;">
|
||||
{% if query %}
|
||||
No programs matched your search.
|
||||
{% else %}
|
||||
There are no programs yet.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Community leader shortcut #}
|
||||
{% if user and user.account_type == 'community_leader' %}
|
||||
<div class="container-full" style="margin-top: 20px; text-align: center;">
|
||||
<p>Want to run a new conservation program?</p>
|
||||
{{ button(url='/programs/create', button_text='Create a Program', extra_classes='homepage-btn') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "base.jinja" %}
|
||||
{% block title %}{{ program.title }}{% endblock %}
|
||||
{% block content %}
|
||||
{% from 'button.jinja' import button %}
|
||||
|
||||
<section id="hero" style="padding: 40px 20px; text-align: center;">
|
||||
<h1>{{ program.title }}</h1>
|
||||
</section>
|
||||
<section style="padding: 0 20px 60px 20px; max-width: 800px; margin: 0 auto;">
|
||||
<div class="container-full">
|
||||
<p style="opacity: 0.8; font-size: 1.05rem; line-height: 1.7; margin-bottom: 1.5rem;">
|
||||
{{ program.description if program.description else 'No description has been provided for this program.' }}
|
||||
</p>
|
||||
<div class="split"></div>
|
||||
<p style="opacity: 0.6; font-size: 0.85rem; margin-top: 1rem;">
|
||||
Program created on {{ program.created_at[:10] if program.created_at else 'unknown date' }}.
|
||||
</p>
|
||||
<div style="margin-top: 1.5rem; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;">
|
||||
{% if enrolled %}
|
||||
<span class="link_button homepage-btn"
|
||||
style="background-color: var(--accent); cursor: default; text-align: center;">
|
||||
✓ You are enrolled
|
||||
</span>
|
||||
{% else %}
|
||||
<form action="/programs/{{ program.program_id }}/join" method="POST" style="display: contents;">
|
||||
<button type="submit" class="link_button homepage-btn"
|
||||
style="border: none; cursor: pointer;">
|
||||
Join This Program
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{{ button(url='/programs', button_text='← All Programs', extra_classes='homepage-btn') }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -3,7 +3,7 @@
|
||||
{# e.g. for the species search: "/species/search" #}
|
||||
{# used in species.jinja #}
|
||||
<form action="{{destination}}" method="GET" class="search">
|
||||
<input type="text" name="search-query" placeholder="Search all species..." value="{{value}}" class="bar">
|
||||
<input type="text" name="search-query" placeholder="Search..." value="{{value}}" class="bar">
|
||||
<input type="submit" value="Search" class="button">
|
||||
</form>
|
||||
{% endmacro %}
|
||||
Reference in new issue
Block a user