merge upstream

This commit is contained in:
TussockyJoker committed 2026-03-14 16:30:03 +00:00
commit e4fb2c4f48
35 files changed
+161 -87

No files matched your search

+44 -10
View File
@@ -3,6 +3,8 @@ from database.db_connection import get_db, get_db
import hashlib
import os
import bcrypt
import secrets
import random
def register_user(username, email, password, account_type="private_user"):
@@ -12,27 +14,59 @@ def register_user(username, email, password, account_type="private_user"):
(username, email)
).fetchone()
if existing:
raise ValueError("A user with that username or email already exists.")
return None
user_id = random.randint(1000000000, 9999999999)
password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(14))
conn.execute("""
INSERT INTO users (username, email, password_hash, account_type, created_at)
VALUES (?, ?, ?, ?, ?)
""", (username, email, password_hash, account_type, datetime.now()))
INSERT INTO users (user_id, username, email, password_hash, account_type, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (user_id, username, email, password_hash, account_type, datetime.now()))
return create_token_for_user(user_id)
def login_user(username, password):
user = None
with get_db() as conn:
row = conn.execute(
"SELECT * FROM users WHERE username = ?",
"SELECT user_id, password_hash FROM users WHERE username = ?",
(username,)
).fetchone()
if not row:
raise ValueError("Invalid username or password.")
return None
user = dict(row)
if not bcrypt.checkpw(password.encode(), user["password_hash"]):
raise ValueError("Invalid username or password.")
return {k: user[k] for k in ("user_id", "username", "email", "account_type")}
return None
return user and create_token_for_user(user["user_id"])
def get_user_from_token(token):
with get_db() as conn:
row = conn.execute(
"SELECT * FROM users WHERE user_id = (SELECT user_id FROM tokens WHERE token = ? LIMIT 1) LIMIT 1",
(token,)
).fetchone()
if not row:
return None
return dict(row)
def create_token_for_user(user_id):
with get_db() as conn:
token = secrets.token_hex(64)
conn.execute("""
INSERT INTO tokens (
token,
user_id
)
VALUES (?, ?)
""", (token, user_id))
return token
print("hello")
def delete_token(token):
with get_db() as conn:
conn.execute("""
DELETE FROM tokens
WHERE token = ?
""", (token,))
def add_species(english, latin, body, category, extinction_risk, image_id):
with get_db() as conn:
@@ -63,7 +97,7 @@ def get_all_species():
def get_species_by_name(species_english):
with get_db() as conn:
row = conn.execute(
"SELECT * FROM species WHERE species_english = ?",
"SELECT * FROM species WHERE species_english = ? LIMIT 1",
(species_english,)
).fetchone()
return dict(row) if row else None
@@ -71,7 +105,7 @@ def get_species_by_name(species_english):
def get_species_by_id(species_id):
with get_db() as conn:
result = conn.execute(
"SELECT * FROM species WHERE species_id = ?",
"SELECT * FROM species WHERE species_id = ? LIMIT 1",
(species_id,)
).fetchone()
if result:
+16 -9
View File
@@ -6,14 +6,20 @@ DATABASE_PATH = "database/database.db"
db = sqlite3.connect(DATABASE_PATH, check_same_thread=False)
db.row_factory = sqlite3.Row
db.execute("""
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
account_type TEXT NOT NULL DEFAULT 'private_user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
account_type TEXT NOT NULL DEFAULT 'private_user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
db.execute("""
CREATE TABLE IF NOT EXISTS tokens (
token TEXT PRIMARY KEY,
user_id INTEGER
)
""")
db.commit()
@@ -23,7 +29,8 @@ def get_db():
try:
yield conn
db.commit()
except:
except Exception:
db.rollback()
raise
finally:
conn.close()
+3 -2
View File
@@ -122,14 +122,15 @@ def seed_species():
continue
conn.execute("""
INSERT INTO species
(species_english, species_latin, body_text, category, extinction_risk, created_at)
VALUES (?, ?, ?, ?, ?, ?)
(species_english, species_latin, body_text, category, extinction_risk, photoid, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
sp["species_english"],
sp["species_latin"],
sp["body_text"],
sp["category"],
sp["extinction_risk"],
sp["photoid"],
datetime.now(),
))
print(f" ADD {sp['species_english']}")
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+35 -4
View File
@@ -1,7 +1,10 @@
from flask import Flask
from flask import Flask, request, g, render_template
import os
import importlib.util
from database.db_init import init_database
from database.db_commands import get_user_from_token
from werkzeug.exceptions import HTTPException
def import_pages(route, app):
for root, _, files in os.walk(route):
@@ -30,11 +33,39 @@ def import_pages(route, app):
app.register_blueprint(module.page)
app = Flask(__name__, static_folder='static', static_url_path='')
app.secret_key = "59474221-13e4-43dd-bc07-46a8d051d456"
app = Flask(__name__, static_folder="static", static_url_path="")
@app.before_request
def load_user():
token = request.cookies.get("token")
g.user = token and get_user_from_token(token)
@app.context_processor
def inject_user():
return {"user": g.user}
@app.errorhandler(404)
def page_not_found(e):
return render_template(
"error.jinja",
extra_information=e,
), 404
@app.errorhandler(Exception)
def handle_bad_request(e):
print(e)
if isinstance(e, HTTPException):
return e
return render_template(
"error.jinja",
extra_information=e,
), 500
init_database()
import_pages("routes", app)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=3000, debug=True)
app.run(host="0.0.0.0", port=3000, debug=True)
+27 -22
View File
@@ -1,4 +1,4 @@
from flask import render_template, Blueprint, request, redirect, url_for, session
from flask import render_template, Blueprint, request, redirect, url_for, Response, g
from database.db_commands import register_user, login_user
page = Blueprint("login", __name__, url_prefix="")
@@ -6,27 +6,27 @@ page = Blueprint("login", __name__, url_prefix="")
@page.route("/login", methods=["GET", "POST"])
def login():
if "user_id" in session:
if g.user:
return redirect(url_for("login.dashboard"))
error = None
if request.method == "POST":
username = request.form.get("username", "").strip()
password = request.form.get("password", "")
try:
user = login_user(username, password)
session.clear()
session["user_id"] = user["user_id"]
session["username"] = user["username"]
session["account_type"] = user["account_type"]
return redirect(url_for("login.dashboard"))
except ValueError as e:
error = str(e)
return render_template("accounts/login.jinja", error=error)
token = login_user(username, password)
if not token:
error = "invalid username or password"
else:
resp = redirect(url_for("login.dashboard"))
resp.set_cookie("token",token)
return resp
resp = Response(render_template("accounts/login.jinja", error=error))
if error: resp.status = 401
return resp
@page.route("/register", methods=["GET", "POST"])
def register():
if "user_id" in session:
if g.user:
return redirect(url_for("login.dashboard"))
error = None
if request.method == "POST":
@@ -40,22 +40,27 @@ def register():
elif len(password) < 8:
error = "Password must be at least 8 characters."
else:
try:
register_user(username, email, password, account_type)
return redirect(url_for("login.login"))
except ValueError as e:
error = str(e)
return render_template("accounts/user_registration.jinja", error=error)
token = register_user(username, email, password, account_type)
if not token:
error = "A user with that username or email already exists."
else:
resp = redirect(url_for("login.dashboard"))
resp.set_cookie("token",token)
return resp
resp = Response(render_template("accounts/user_registration.jinja", error=error))
if error: resp.status = 401
return resp
@page.route("/logout")
def logout():
session.clear()
return redirect(url_for("login.login"))
resp = redirect(url_for("login.login"))
resp.delete_cookie("token")
return resp
@page.route("/dashboard")
def dashboard():
if "user_id" not in session:
if not g.user:
return redirect(url_for("login.login"))
return render_template("accounts/dashboard.jinja")
+19 -21
View File
@@ -1,5 +1,5 @@
from flask import render_template, Blueprint
from database.db_commands import get_species_by_name
from flask import render_template, Blueprint, send_from_directory
from database.db_commands import get_species_by_name, get_species_by_id
import sqlite3
page = Blueprint("species_information", __name__)
@@ -15,22 +15,20 @@ page = Blueprint("species_information", __name__)
@page.route("/species/<species_english>")
def data(species_english):
try:
database_entry = get_species_by_name(species_english)
if database_entry is None:
raise TypeError(f"No species named '{species_english}' found in the database.")
return render_template(
"species_information/species_information.jinja",
english_name =(database_entry["species_english"]).capitalize(),
latin_name =database_entry["species_latin"].capitalize(),
main_text =database_entry["body_text"],
category =database_entry["category"].capitalize(),
extinction_risk =database_entry["extinction_risk"],
photoid =database_entry["photoid"],
species_id =str(database_entry["species_id"])
)
except TypeError as error_information:
return render_template(
"error.jinja",
extra_information=error_information,
)
database_entry = get_species_by_name(species_english)
if database_entry is None:
raise TypeError(f"No species named '{species_english}' found in the database.")
return render_template(
"species_information/species_information.jinja",
english_name =(database_entry["species_english"]).capitalize(),
latin_name =database_entry["species_latin"].capitalize(),
main_text =database_entry["body_text"],
category =database_entry["category"].capitalize(),
extinction_risk =database_entry["extinction_risk"],
photoid =database_entry["photoid"],
species_id =str(database_entry["species_id"])
)
@page.route("/species/image/<photoid>")
def image(photoid):
return send_from_directory("database/images", photoid)
+1 -1
View File
@@ -29,7 +29,7 @@ def species_update(species_id):
if species_image and species_image.filename:
image_id = str(uuid.uuid4())
species_image.save(f"static/images/species_database/{image_id}.jpg")
species_image.save(f"database/images/{image_id}")
update_species(species_id, eng_name, latin_name, main_text, category, extinction_risk)
+1 -1
View File
@@ -23,7 +23,7 @@ def add_species_to_database():
image_id = ""
if species_image:
image_id = str(uuid.uuid4())
species_image.save(f"static/images/species_database/{image_id}.jpg")
species_image.save(f"database/images/{image_id}")
print(category, extinction_risk)
add_species(eng_name, latin_name, main_text, category, extinction_risk, image_id)
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+3 -6
View File
@@ -57,7 +57,7 @@ h6 {
.content {
max-width: 2000px;
margin: 0 auto;
margin: 2rem auto;
padding: 0 1rem 1rem;
}
@@ -66,7 +66,7 @@ h6 {
position: sticky;
top: 0;
background-color: var(--transparent);
backdrop-filter: blur(50px);
backdrop-filter: blur(15px);
height: var(--navbar-height);
min-height: 40px;
padding: 0.5rem 1rem;
@@ -75,6 +75,7 @@ h6 {
justify-content: space-between;
align-items: center;
z-index: 998;
max-height: 50px;
}
.navbar .link_button {
@@ -174,10 +175,6 @@ h6 {
text-decoration: none;
}
.logo::before {
content: "The Sovereign Bank of Libreland";
}
.welcome {
display: flex;
background-image: url("/1706110852106.jpg");
+3 -3
View File
@@ -5,7 +5,7 @@
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>Welcome back, {{ session.username }}.</h1>
<h1>Welcome back, {{ user.username }}.</h1>
<p style="max-width: 700px; margin: 0.5rem auto;">Your conservation dashboard.</p>
</section>
@@ -40,7 +40,7 @@
{% endif %}
{#Teacher#}
{% if session.account_type == "teacher" %}
{% if user.account_type == "teacher" %}
<div class="container-full" style="margin-bottom: 20px;">
<h2>Classes</h2>
<p>View your classes or set up a new one for your students.</p>
@@ -51,7 +51,7 @@
{% endif %}
{#Community Leader#}
{% if session.account_type == "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>
+1 -1
View File
@@ -6,7 +6,7 @@
<a href="/species/{{ item['species_english'] }}" class="container-lite">
<h3 class="plain-white-text">{{ item['species_english'] }}</h3>
<div
style="background-image: url('{{ url_for('static', filename='images/species_database/' ~ item['photoid'] ~ '.jpg') }}');" class="div-image">
style="background-image: url('{{ '/species/image/' ~ item['photoid'] }}');" class="div-image">
</div>
<p style="color: var(--text)">
Main category: {{ item['category'] }}
+1 -1
View File
@@ -11,7 +11,7 @@
{%for species in featured_species%}
<a href="/species/{{ species['species_english'] }}" class="container-lite">
<h3 class="plain-white-text">{{ species['species_english'] }}</h3>
<div style="background-image: url('images/species_database/{{ species['photoid'] }}.jpg');" class="div-image">
<div style="background-image: url('/species/image/{{ species['photoid'] }}');" class="div-image">
</div>
</a>
+1
View File
@@ -26,6 +26,7 @@
<a href="/admin">Admin</a>
{% endif %}
<a href="/dashboard">Dashboard</a>
<a href="/settings">Settings</a>
<a href="/logout">Logout</a>
{% else %}
{{ button(url='/login', button_text='Login', extra_classes='') }}
+2 -2
View File
@@ -1,9 +1,9 @@
{% macro search_bar(destination)%}
{% macro search_bar(destination, value)%}
{# input the url of the page containing the backend for the search #}
{# 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..." class="bar">
<input type="text" name="search-query" placeholder="Search all species..." value="{{value}}" class="bar">
<input type="submit" value="Search" class="button">
</form>
{% endmacro %}
+1 -1
View File
@@ -14,7 +14,7 @@ Species Database
{# If you want to change the featured species please do so in config.ini - CS #}
{{featured_species_section(featured_species, False)}}
{# All Species #}
{{search_bar("/species/search")}}
{{search_bar("/species/search", "")}}
<h2 style="text-align: center; margin-top: 30px;">All Species</h2>
{{display_all_items(species)}}
{% endblock %}
+1 -1
View File
@@ -10,6 +10,6 @@
Species Database
</section>
{# All Species #}
{{search_bar("/species/search")}}
{{search_bar("/species/search", query)}}
{{display_all_items(results)}}
{% endblock %}
@@ -9,7 +9,7 @@
<h1>{{ english_name }}</h1>
<h2><em>{{ latin_name }}</em></h2>
<p>Category: {{category}}</p>
<img src="/images/species_database/{{ photoid }}.jpg" class="species-photo" style="max-width: 100%; height: auto; border-radius: inherit;">
<img src="/species/image/{{ photoid }}" class="species-photo" style="max-width: 100%; height: auto; border-radius: inherit;">
<p>This species is currently classed as: {{extinction_risk}}</p>
{% if extinction_risk == "Not Evaluated" %}
<img src="/images/extinction_risk/notevaluated.png" class="extinction-icon">
@@ -8,7 +8,7 @@
<div style="display:flex;flex-direction: row;">
<div class="container" style="height: fit-content; width:fit-content; text-align:center; max-width: 40%; padding:20px">
<p> Current photo: </p>
<img src="/images/species_database/{{ sp['photoid'] }}.jpg" style="width: 100%; height: auto; margin: auto">
<img src="/species/image/{{ sp['photoid'] }}" style="width: 100%; height: auto; margin: auto">
</div>
<section id="hero" class="container">
<form action="{{ url_for('species_editor.species_update', species_id=species_id) }}" class="form" method="POST"