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
|
# uv specific
|
||||||
.uvcache/
|
.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,9 +22,10 @@ build-backend = "uv_build"
|
|||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"black>=25.9.0",
|
"black>=25.9.0",
|
||||||
|
"httpx>=0.28.1",
|
||||||
"isort>=7.0.0",
|
"isort>=7.0.0",
|
||||||
"pytest>=8.4.2",
|
"pytest>=8.4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
# backend/app/main.py
|
import os
|
||||||
from fastapi import FastAPI, Request
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
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")
|
templates = Jinja2Templates(directory="src/allmende_payment_system/templates")
|
||||||
app.mount(
|
app.mount(
|
||||||
"/static",
|
"/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("/")
|
@app.get("/")
|
||||||
async def landing_page(request: Request):
|
async def landing_page(request: Request, user_info: UserDep, session: SessionDep):
|
||||||
return templates.TemplateResponse("index.html.jinja", {"request": request})
|
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 import create_engine, select
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
|
|
||||||
|
from allmende_payment_system.models import User
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./aps_db.db"
|
SQLALCHEMY_DATABASE_URL = "sqlite:///./aps_db.db"
|
||||||
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
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",
|
back_populates="accounts",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def balance(self):
|
||||||
|
return 141.23
|
||||||
|
|
||||||
|
|
||||||
user_account_association = Table(
|
user_account_association = Table(
|
||||||
TABLE_PREFIX + "user_account_association",
|
TABLE_PREFIX + "user_account_association",
|
||||||
|
|||||||
@@ -1,7 +1,75 @@
|
|||||||
|
|
||||||
{% extends "base.html.jinja" %}
|
{% extends "base.html.jinja" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="p-3">
|
<!-- Account Balances Section -->
|
||||||
<h1>Welcome to My App</h1>
|
<div class="mb-4">
|
||||||
<p>This is your landing page content. Replace this with your actual content.</p>
|
<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>
|
</div>
|
||||||
{% endblock %}
|
{% 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
|
from allmende_payment_system.models import Account, User
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_user(in_memory_db):
|
def test_user_model(test_db):
|
||||||
user = User(username="test", display_name="Test User")
|
user = User(username="test", display_name="Test User")
|
||||||
in_memory_db.add(user)
|
test_db.add(user)
|
||||||
in_memory_db.commit()
|
test_db.commit()
|
||||||
|
|
||||||
assert user.id is not None
|
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]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "black" },
|
{ name = "black" },
|
||||||
|
{ name = "httpx" },
|
||||||
{ name = "isort" },
|
{ name = "isort" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
]
|
]
|
||||||
@@ -27,6 +28,7 @@ requires-dist = [
|
|||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "black", specifier = ">=25.9.0" },
|
{ name = "black", specifier = ">=25.9.0" },
|
||||||
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
{ name = "isort", specifier = ">=7.0.0" },
|
{ name = "isort", specifier = ">=7.0.0" },
|
||||||
{ name = "pytest", specifier = ">=8.4.2" },
|
{ name = "pytest", specifier = ">=8.4.2" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user