Compare commits
3 Commits
a1563b53ac
...
81929cca21
| Author | SHA1 | Date | |
|---|---|---|---|
| 81929cca21 | |||
| f6e69b1521 | |||
| e1c8b4ebeb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -89,3 +89,4 @@ instance/
|
||||
|
||||
# uv specific
|
||||
.uvcache/
|
||||
/db_fixtures/
|
||||
|
||||
7
dev-server.sh
Normal file
7
dev-server.sh
Normal file
@@ -0,0 +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
|
||||
8
justfile
Normal file
8
justfile
Normal file
@@ -0,0 +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
|
||||
@@ -22,6 +22,7 @@ build-backend = "uv_build"
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"black>=25.9.0",
|
||||
"httpx>=0.28.1",
|
||||
"isort>=7.0.0",
|
||||
"pytest>=8.4.2",
|
||||
]
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
# backend/app/main.py
|
||||
from fastapi import FastAPI, Request
|
||||
import os
|
||||
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,
|
||||
"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"]}
|
||||
|
||||
|
||||
UserDep = Annotated[dict, Depends(get_user)]
|
||||
|
||||
|
||||
app = FastAPI(dependencies=[Depends(get_user)])
|
||||
|
||||
app = FastAPI()
|
||||
templates = Jinja2Templates(directory="src/allmende_payment_system/templates")
|
||||
app.mount(
|
||||
"/static",
|
||||
@@ -12,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}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 %}
|
||||
35
test/conftest.py
Normal file
35
test/conftest.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
|
||||
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():
|
||||
os.environ["APS_username"] = "test"
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def unauthorized_client():
|
||||
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()
|
||||
11
test/test_auth.py
Normal file
11
test/test_auth.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from allmende_payment_system.models import Account
|
||||
|
||||
|
||||
def test_unauthorized_access(unauthorized_client):
|
||||
response = unauthorized_client.get("/")
|
||||
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
19
test/test_database.py
Normal 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
|
||||
@@ -1,27 +1,16 @@
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
from allmende_payment_system.models import Base, 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()
|
||||
from allmende_payment_system.models import Account, User
|
||||
|
||||
|
||||
def test_create_user(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)
|
||||
test_db.add(account)
|
||||
test_db.commit()
|
||||
|
||||
assert len(user.accounts) == 1
|
||||
|
||||
2
uv.lock
generated
2
uv.lock
generated
@@ -14,6 +14,7 @@ dependencies = [
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "black" },
|
||||
{ name = "httpx" },
|
||||
{ name = "isort" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
@@ -27,6 +28,7 @@ requires-dist = [
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "black", specifier = ">=25.9.0" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "isort", specifier = ">=7.0.0" },
|
||||
{ name = "pytest", specifier = ">=8.4.2" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user