Compare commits
2 Commits
8fd8b710fb
...
b3166811e5
| Author | SHA1 | Date | |
|---|---|---|---|
| b3166811e5 | |||
| bd2f7b286e |
0
dev-server.sh
Normal file → Executable file
0
dev-server.sh
Normal file → Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
from fastapi import APIRouter, Request
|
||||||
|
|
||||||
|
from allmende_payment_system.api.dependencies import SessionDep, UserDep
|
||||||
|
from allmende_payment_system.database import ensure_user
|
||||||
|
from allmende_payment_system.tools import get_jinja_renderer
|
||||||
|
|
||||||
|
root_router = APIRouter()
|
||||||
|
|
||||||
|
templates = get_jinja_renderer()
|
||||||
|
|
||||||
|
|
||||||
|
@root_router.get("/")
|
||||||
|
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")
|
||||||
|
transactions = []
|
||||||
|
for account in user.accounts:
|
||||||
|
transactions += account.transactions
|
||||||
|
transactions = sorted(transactions, key=lambda t: t.timestamp)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"index.html.jinja",
|
||||||
|
context={"request": request, "user": user, "transactions": transactions},
|
||||||
|
)
|
||||||
|
|||||||
33
src/allmende_payment_system/api/dependencies.py
Normal file
33
src/allmende_payment_system/api/dependencies.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import os
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException, Request
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from allmende_payment_system.database import SessionLocal
|
||||||
|
|
||||||
|
|
||||||
|
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)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_session() -> Session:
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
SessionDep = Annotated[Session, Depends(get_session)]
|
||||||
30
src/allmende_payment_system/api/shop.py
Normal file
30
src/allmende_payment_system/api/shop.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from allmende_payment_system.api import SessionDep
|
||||||
|
from allmende_payment_system.models import Area
|
||||||
|
from allmende_payment_system.tools import get_jinja_renderer
|
||||||
|
|
||||||
|
shop_router = APIRouter()
|
||||||
|
|
||||||
|
templates = get_jinja_renderer()
|
||||||
|
|
||||||
|
|
||||||
|
@shop_router.get("/shop")
|
||||||
|
async def get_shop(request: Request, session: SessionDep):
|
||||||
|
query = select(Area)
|
||||||
|
areas = session.scalars(query).all()
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"shop.html.jinja",
|
||||||
|
context={"request": request, "areas": areas},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@shop_router.get("/shop/area/{area_id}")
|
||||||
|
async def get_shop(request: Request, session: SessionDep, area_id: int):
|
||||||
|
query = select(Area).where(Area.id == area_id)
|
||||||
|
area = session.scalars(query).one()
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"area.html.jinja",
|
||||||
|
context={"request": request, "area": area},
|
||||||
|
)
|
||||||
@@ -1,32 +1,16 @@
|
|||||||
import os
|
import locale
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
from fastapi import Depends, FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
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)]
|
|
||||||
|
|
||||||
|
from allmende_payment_system.api import root_router
|
||||||
|
from allmende_payment_system.api.dependencies import get_user
|
||||||
|
from allmende_payment_system.api.shop import shop_router
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
|
||||||
app = FastAPI(dependencies=[Depends(get_user)])
|
app = FastAPI(dependencies=[Depends(get_user)])
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="src/allmende_payment_system/templates")
|
|
||||||
app.mount(
|
app.mount(
|
||||||
"/static",
|
"/static",
|
||||||
StaticFiles(directory="src/allmende_payment_system/static"),
|
StaticFiles(directory="src/allmende_payment_system/static"),
|
||||||
@@ -34,26 +18,5 @@ app.mount(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_session() -> Session:
|
app.include_router(root_router)
|
||||||
db = SessionLocal()
|
app.include_router(shop_router)
|
||||||
try:
|
|
||||||
yield db
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
|
|
||||||
SessionDep = Annotated[Session, Depends(get_session)]
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
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")
|
|
||||||
transactions = []
|
|
||||||
for account in user.accounts:
|
|
||||||
transactions += account.transactions
|
|
||||||
transactions = sorted(transactions, key=lambda t: t.timestamp)
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"index.html.jinja",
|
|
||||||
context={"request": request, "user": user, "transactions": transactions},
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -52,6 +52,17 @@ class Area(Base):
|
|||||||
__tablename__ = TABLE_PREFIX + "area"
|
__tablename__ = TABLE_PREFIX + "area"
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
name: Mapped[str] = mapped_column(nullable=False, unique=True)
|
name: Mapped[str] = mapped_column(nullable=False, unique=True)
|
||||||
|
description: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
image_path: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
||||||
|
products: Mapped[list["Product"]] = relationship("Product")
|
||||||
|
|
||||||
|
|
||||||
|
UnitsOfMeasure = typing.Literal[
|
||||||
|
"g",
|
||||||
|
"kg",
|
||||||
|
"piece",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Product(Base):
|
class Product(Base):
|
||||||
@@ -60,11 +71,14 @@ class Product(Base):
|
|||||||
name: Mapped[str] = mapped_column(nullable=False, unique=True)
|
name: Mapped[str] = mapped_column(nullable=False, unique=True)
|
||||||
|
|
||||||
price: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
price: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
||||||
|
unit_of_measure: Mapped[UnitsOfMeasure] = mapped_column(nullable=False)
|
||||||
# TODO: limit this to actually used vat rates?
|
# TODO: limit this to actually used vat rates?
|
||||||
vat_rate: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
vat_rate: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
||||||
|
|
||||||
area_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "area.id"))
|
area_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "area.id"))
|
||||||
area: Mapped["Area"] = relationship("Area")
|
area: Mapped["Area"] = relationship("Area", back_populates="products")
|
||||||
|
|
||||||
|
image_path: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
||||||
|
|
||||||
TransactionTypes = typing.Literal[
|
TransactionTypes = typing.Literal[
|
||||||
|
|||||||
BIN
src/allmende_payment_system/static/img/food-coop.jpg
Normal file
BIN
src/allmende_payment_system/static/img/food-coop.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
BIN
src/allmende_payment_system/static/img/gemeinsam-essen.jpg
Normal file
BIN
src/allmende_payment_system/static/img/gemeinsam-essen.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
BIN
src/allmende_payment_system/static/img/kaffeemaschine.jpg
Normal file
BIN
src/allmende_payment_system/static/img/kaffeemaschine.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
src/allmende_payment_system/static/img/placeholder.jpg
Normal file
BIN
src/allmende_payment_system/static/img/placeholder.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/allmende_payment_system/static/img/waschküche.jpg
Normal file
BIN
src/allmende_payment_system/static/img/waschküche.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
36
src/allmende_payment_system/templates/area.html.jinja
Normal file
36
src/allmende_payment_system/templates/area.html.jinja
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{% extends "base.html.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<!-- Area Header -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="h4 mb-3">{{ area.name }}</h2>
|
||||||
|
<p class="text-muted">{{ area.description or ''}} </p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Products Grid -->
|
||||||
|
<div class="row g-3">
|
||||||
|
{% for product in area.products %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
|
<a href="#" class="text-decoration-none text-dark">
|
||||||
|
<!-- Product Image -->
|
||||||
|
<img
|
||||||
|
src="/static/img/{{ product.image_path if product.image_path else 'placeholder.jpg' }}"
|
||||||
|
alt="{{ product.name }}"
|
||||||
|
class="card-img-top img-fluid rounded-top"
|
||||||
|
style="height: 100px; object-fit: cover;"
|
||||||
|
>
|
||||||
|
<!-- Product Details -->
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-2">{{ product.name }}</h5>
|
||||||
|
<p class="card-text text-muted small mb-3">{{ product.description }}</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span class="fw-bold">{{ product.price|format_number }} € pro {{ product.unit_of_measure|units_of_measure_de }}</span>
|
||||||
|
<button class="btn btn-sm btn-outline-primary">Add to Cart</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -21,22 +21,22 @@
|
|||||||
<ul class="nav nav-pills flex-column mb-auto">
|
<ul class="nav nav-pills flex-column mb-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="#" class="nav-link active">
|
<a href="#" class="nav-link active">
|
||||||
Dashboard
|
Übersicht
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/shop" class="nav-link">
|
||||||
|
Einkaufen
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="#" class="nav-link">
|
<a href="#" class="nav-link">
|
||||||
Projects
|
Lorem
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="#" class="nav-link">
|
<a href="#" class="nav-link">
|
||||||
Reports
|
Ipsum
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="#" class="nav-link">
|
|
||||||
Settings
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
{% for transaction in transactions[:10] %}
|
{% for transaction in transactions[:10] %}
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-start py-3">
|
<div class="list-group-item d-flex justify-content-between align-items-start py-3">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<div class="fw-semibold">{{ transaction.product.name }}</div>
|
<div class="fw-semibold">{{ transaction.product.name or transaction.type|transaction_type_de }}</div>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
{{ transaction.timestamp }}
|
{{ transaction.timestamp }}
|
||||||
</small>
|
</small>
|
||||||
|
|||||||
61
src/allmende_payment_system/templates/shop.html.jinja
Normal file
61
src/allmende_payment_system/templates/shop.html.jinja
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{% extends "base.html.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<!-- Shop Landing Page Header -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="h4 mb-3">Shop</h2>
|
||||||
|
<p class="text-muted">In welchem Bereich möchtest du einkaufen?</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
{% for area in areas %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<a href="/shop/area/{{ area.id }}" class="text-decoration-none">
|
||||||
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
|
<div class="card-body d-flex align-items-center p-3">
|
||||||
|
<!-- Image on the left -->
|
||||||
|
<div class="me-3" style="width: 120px; flex-shrink: 0;">
|
||||||
|
<img
|
||||||
|
src="/static/img/{{ area.image_path if area.image_path !='' else 'placeholder.png'}}" }}"
|
||||||
|
alt="{{ area.name }}"
|
||||||
|
class="img-fluid rounded"
|
||||||
|
style="max-height: 100px; width: 100%; object-fit: contain;"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<!-- Title and description on the right -->
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="card-title mb-1">{{ area.name }}</h5>
|
||||||
|
<p class="card-text text-muted small mb-0">{{ area.description or '' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Optional: Featured Products Section -->
|
||||||
|
{# <div class="mt-5">#}
|
||||||
|
{# <div class="d-flex justify-content-between align-items-center mb-3">#}
|
||||||
|
{# <h2 class="h4 mb-0">Featured Products</h2>#}
|
||||||
|
{# <a href="#" class="btn btn-outline-primary btn-sm">View All</a>#}
|
||||||
|
{# </div>#}
|
||||||
|
{# <div class="alert alert-info">#}
|
||||||
|
{# Featured products will appear here#}
|
||||||
|
{# </div>#}
|
||||||
|
{# </div>#}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<style>
|
||||||
|
/* Hover effect for shop tiles */
|
||||||
|
.hover-shadow:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
28
src/allmende_payment_system/tools.py
Normal file
28
src/allmende_payment_system/tools.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import locale
|
||||||
|
import numbers
|
||||||
|
|
||||||
|
from starlette.templating import Jinja2Templates
|
||||||
|
|
||||||
|
TRANSACTION_TYPE_DE = {
|
||||||
|
"deposit": "Einzahlung",
|
||||||
|
"withdrawal": "Auszahlung",
|
||||||
|
"expense": "Auslage",
|
||||||
|
"product": "Einkauf",
|
||||||
|
}
|
||||||
|
|
||||||
|
UNITS_OF_MEASURE = {"piece": "Stück"}
|
||||||
|
|
||||||
|
|
||||||
|
def format_number(value: float):
|
||||||
|
try:
|
||||||
|
return f"{value:n}"
|
||||||
|
except TypeError:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def get_jinja_renderer() -> Jinja2Templates:
|
||||||
|
renderer = Jinja2Templates(directory="src/allmende_payment_system/templates")
|
||||||
|
renderer.env.filters["transaction_type_de"] = lambda x: TRANSACTION_TYPE_DE[x]
|
||||||
|
renderer.env.filters["units_of_measure_de"] = lambda x: UNITS_OF_MEASURE.get(x, x)
|
||||||
|
renderer.env.filters["format_number"] = format_number
|
||||||
|
return renderer
|
||||||
Reference in New Issue
Block a user