Add first info to landing page

This commit is contained in:
2025-10-23 12:35:52 +02:00
parent f6e69b1521
commit 81929cca21
11 changed files with 186 additions and 57 deletions

1
.gitignore vendored
View File

@@ -89,3 +89,4 @@ instance/
# uv specific
.uvcache/
/db_fixtures/

View File

@@ -1,4 +1,7 @@
if [ -z "${APS_username}" ]; then
export APS_username="testuser"
fi
if [ -z "${APS_display_name}" ]; then
export APS_display_name="Dr. T. Estuser"
fi
fastapi dev src/allmende_payment_system/app.py

View File

@@ -1,3 +1,8 @@
lint:
uv run isort test src
uv run black test src
reset_db:
rm -f aps_db.db
uv run python -c "from allmende_payment_system.database import create_tables; create_tables()"
for file in db_fixtures/*.sql; do sqlite3 aps_db.db < "$file"; done

View File

@@ -4,12 +4,18 @@ from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from allmende_payment_system.database import SessionLocal, ensure_user
async def get_user(request: Request) -> dict:
if username := os.environ.get("APS_username", None):
return {"username": username}
return {
"username": username,
"display_name": os.environ.get("APS_display_name", "Missing Display Name"),
}
if "ynh_user" not in request.headers:
raise HTTPException(status_code=401, detail="Missing ynh_user header")
return {"username": request.headers["ynh_user"]}
@@ -28,6 +34,21 @@ app.mount(
)
def get_session() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
SessionDep = Annotated[Session, Depends(get_session)]
@app.get("/")
async def landing_page(request: Request):
return templates.TemplateResponse("index.html.jinja", {"request": request})
async def landing_page(request: Request, user_info: UserDep, session: SessionDep):
user = ensure_user(user_info, session)
print(f"User {user.username} ({user.display_name}) accessed landing page")
return templates.TemplateResponse(
"index.html.jinja", context={"request": request, "user": user}
)

View File

@@ -1,6 +1,29 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session, sessionmaker
from allmende_payment_system.models import User
SQLALCHEMY_DATABASE_URL = "sqlite:///./aps_db.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def create_tables():
from allmende_payment_system.models import Base
Base.metadata.create_all(bind=engine)
def ensure_user(user_info: dict, session: Session) -> User:
statement = select(User).where(User.username == user_info["username"])
if user := session.scalars(statement).one_or_none():
return user
user = User(
username=user_info["username"], display_name=user_info.get("display_name")
)
session.add(user)
session.commit()
return user

View File

@@ -19,6 +19,10 @@ class Account(Base):
back_populates="accounts",
)
@property
def balance(self):
return 141.23
user_account_association = Table(
TABLE_PREFIX + "user_account_association",

View File

@@ -1,7 +1,75 @@
{% extends "base.html.jinja" %}
{% block content %}
<div class="p-3">
<h1>Welcome to My App</h1>
<p>This is your landing page content. Replace this with your actual content.</p>
<!-- Account Balances Section -->
<div class="mb-4">
<h2 class="h4 mb-3">Meine Konten</h2>
<div class="row g-3">
{% if user.accounts|length > 0 %}
{% for account in user.accounts %}
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ account.name }}</h5>
<p class="h3 mb-2 {% if account.balance < 0 %}text-danger{% else %}text-success{% endif %}">
{{ '%.2f' | format(account.balance) }} €
</p>
<div class="d-flex gap-2 mt-3">
<a href="#" class="btn btn-sm btn-outline-primary">Transaktionen</a>
<a href="#" class="btn btn-sm btn-success">Aufladen</a>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-12">
<div class="alert alert-info">
Keine Konten verfügbar
</div>
</div>
{% endif %}
</div>
</div>
<!-- Latest Transactions Section -->
<div class="transactions-section">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h3 mb-0">Latest Transactions</h2>
<a href="#" class="btn btn-outline-primary btn-sm">View All</a>
</div>
<div class="card">
<div class="list-group list-group-flush">
{% if transactions and transactions|length > 0 %}
{% for transaction in transactions[:5] %}
<div class="list-group-item d-flex justify-content-between align-items-start py-3">
<div class="flex-grow-1">
<div class="fw-semibold">{{ transaction.product_name | default('Transaction') }}</div>
<small class="text-muted">
{{ transaction.date | default('') }}
{% if transaction.description %}
· {{ transaction.description }}
{% endif %}
</small>
</div>
<div class="text-end ms-3">
<span class="fs-5 fw-bold {% if transaction.amount < 0 %}text-danger{% else %}text-success{% endif %}">
{{ '%+.2f' | format(transaction.amount | default(0)) }} €
</span>
{% if transaction.quantity %}
<div class="small text-muted">{{ transaction.quantity }} {{ transaction.unit | default('pcs') }}</div>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="list-group-item text-center py-5 text-muted">
<p class="mb-0">No transactions yet</p>
<small>Your transactions will appear here</small>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,30 +1,35 @@
import pytest
from fastapi import Request
from fastapi.testclient import TestClient
import os
from allmende_payment_system.app import create_app
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from allmende_payment_system.app import app
from allmende_payment_system.models import Base
@pytest.fixture(scope="session")
def client():
app = create_app()
async def add_ynh_headers(request: Request, call_next):
username = request.headers.get("APS-TEST-username", "test")
# This seems to work although headers are immutable
# If this ever turns out to be a problem, we can use request.state instead,
# but will have to modify app.get_user
request.headers._list.append((b"ynh_user", username.encode("utf-8")))
response = await call_next(request)
return response
app.middleware("http")(add_ynh_headers)
os.environ["APS_username"] = "test"
return TestClient(app)
@pytest.fixture(scope="session")
def unauthorized_client():
app = create_app()
os.environ.pop("APS_username", None)
return TestClient(app)
@pytest.fixture
def test_db():
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(bind=engine) # Create tables
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Provide a session and the engine
db = TestingSessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -6,7 +6,6 @@ def test_unauthorized_access(unauthorized_client):
assert response.status_code == 401
def test_unauthorized_access(unauthorized_client):
response = unauthorized_client.get("/")
print(response.text)
assert response.status_code == 401
def test_authorized_access(client):
response = client.get("/")
assert response.status_code == 200

19
test/test_database.py Normal file
View File

@@ -0,0 +1,19 @@
from sqlalchemy import func, select
from allmende_payment_system.database import ensure_user
from allmende_payment_system.models import User
def test_ensure_user(test_db):
user_info = {"username": "test", "display_name": "Test User"}
user = ensure_user(user_info, test_db)
assert user.username == "test"
test_db.commit()
assert test_db.scalar(select(func.count()).select_from(User)) == 1
user = ensure_user(user_info, test_db)
assert test_db.scalar(select(func.count()).select_from(User)) == 1

View File

@@ -1,35 +1,16 @@
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from allmende_payment_system.models import Account, Base, User
from allmende_payment_system.models import Account, User
# Create an in-memory SQLite database
@pytest.fixture
def in_memory_db():
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(bind=engine) # Create tables
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Provide a session and the engine
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
def test_user_model(in_memory_db):
def test_user_model(test_db):
user = User(username="test", display_name="Test User")
in_memory_db.add(user)
in_memory_db.commit()
test_db.add(user)
test_db.commit()
assert user.id is not None
account = Account(name="Test Account")
account.users.append(user)
in_memory_db.add(account)
in_memory_db.commit()
test_db.add(account)
test_db.commit()
assert len(user.accounts) == 1