added programs #60

Merged
Ugric merged 1 commits from TommyHEC/Komodo-Hub:main into main 2026-03-30 01:54:26 +01:00
9 changed files with 484 additions and 27 deletions

No files matched your search

+76 -1
View File
@@ -327,4 +327,79 @@ def mark_submission(submission_id, marks, feedback=""):
UPDATE submissions
SET marks = ?, feedback = ?, marked_at = ?
WHERE submission_id = ?
""", (marks, feedback, datetime.now(), 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]
+28 -4
View File
@@ -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():
@@ -198,7 +199,30 @@ def init_classes_database():
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_classes_database()
init_classes_database()
init_programs_database()
+127
View File
@@ -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))
+19 -21
View File
@@ -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 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 %}
+47
View File
@@ -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 %}
+66
View File
@@ -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 %}
+84
View File
@@ -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>
&mdash; <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 %}
+36
View File
@@ -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 %}
+1 -1
View File
@@ -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 %}