View order
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from starlette import status
|
from starlette import status
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
|
|
||||||
from allmende_payment_system.api.dependencies import SessionDep, UserDep
|
from allmende_payment_system.api.dependencies import SessionDep, UserDep
|
||||||
from allmende_payment_system.models import Area, OrderItem, Product
|
from allmende_payment_system.models import Area, Order, OrderItem, Product
|
||||||
from allmende_payment_system.tools import get_jinja_renderer
|
from allmende_payment_system.tools import get_jinja_renderer
|
||||||
|
|
||||||
shop_router = APIRouter()
|
shop_router = APIRouter()
|
||||||
@@ -28,8 +28,8 @@ async def get_shop(request: Request, session: SessionDep):
|
|||||||
async def get_cart(request: Request, session: SessionDep, user: UserDep):
|
async def get_cart(request: Request, session: SessionDep, user: UserDep):
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"cart.html.jinja",
|
"order.html.jinja",
|
||||||
context={"request": request, "user": user},
|
context={"request": request, "order": user.shopping_cart, "is_cart": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -76,3 +76,22 @@ async def add_to_cart(request: Request, session: SessionDep, user: UserDep):
|
|||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
url=f"/shop/area/{form_data['area_id']}", status_code=status.HTTP_302_FOUND
|
url=f"/shop/area/{form_data['area_id']}", status_code=status.HTTP_302_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@shop_router.get("/shop/order/{order_id}")
|
||||||
|
async def add_to_cart(
|
||||||
|
request: Request, session: SessionDep, user: UserDep, order_id: int
|
||||||
|
):
|
||||||
|
|
||||||
|
query = select(Order).where(Order.id == order_id)
|
||||||
|
order = session.scalars(query).one()
|
||||||
|
|
||||||
|
if user.id != order.user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403, detail=f"User not authorized to view this order."
|
||||||
|
)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"order.html.jinja",
|
||||||
|
context={"request": request, "order": order, "is_cart": False},
|
||||||
|
)
|
||||||
|
|||||||
@@ -45,10 +45,15 @@
|
|||||||
{% 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.type|transaction_type_de }}</div>
|
<div class="fw-semibold d-inline">{{ transaction.type|transaction_type_de }}</div>
|
||||||
<small class="text-muted">
|
<small class="text-muted d-inline ms-2">
|
||||||
{{ transaction.timestamp }}
|
{{ transaction.timestamp | timestamp_de }}
|
||||||
</small>
|
</small>
|
||||||
|
{% if transaction.type == "order" %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="/shop/order/{{ transaction.order_id }}" class="btn btn-sm btn-outline-primary">Einkauf ansehen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end ms-3">
|
<div class="text-end ms-3">
|
||||||
<span class="fs-5 fw-bold {% if transaction.total_amount < 0 %}text-danger{% else %}text-success{% endif %}">
|
<span class="fs-5 fw-bold {% if transaction.total_amount < 0 %}text-danger{% else %}text-success{% endif %}">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{% extends "base.html.jinja" %}
|
{% extends "base.html.jinja" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="h4 mb-3">Warenkorb</h2>
|
<h2 class="h4 mb-3">{% if is_cart %}Warenkorb{% else %}Einkauf #{{ order.id }}{% endif %}</h2>
|
||||||
<p class="text-muted">Überprüfe deine Artikel und fahre zur Kasse fort.</p>
|
{% if not is_cart %}<p class="text-muted">Einkauf abgeschickt: {{ order.transaction.timestamp | timestamp_de }}</p>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set items = user.shopping_cart.items %}
|
{% set items = order.items %}
|
||||||
|
|
||||||
{% if items|length == 0 %}
|
{% if items|length == 0 %}
|
||||||
<div class="alert alert-info">Dein Warenkorb ist leer. <a href="/shop" class="alert-link">Weiter einkaufen</a>.</div>
|
<div class="alert alert-info">Dein Warenkorb ist leer. <a href="/shop" class="alert-link">Weiter einkaufen</a>.</div>
|
||||||
@@ -34,15 +34,19 @@
|
|||||||
<div class="text-muted small">{{ item.description or '' }}</div>
|
<div class="text-muted small">{{ item.description or '' }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center" style="width:180px;">
|
<td class="text-center" style="width:180px;">
|
||||||
|
{% if is_cart %}
|
||||||
<form method="post" action="/cart/update/{{ item.id }}" class="d-flex align-items-center justify-content-center">
|
<form method="post" action="/cart/update/{{ item.id }}" class="d-flex align-items-center justify-content-center">
|
||||||
<input type="number" name="quantity" value="{{ qty }}" min="1" step="0.01" class="form-control form-control-sm me-2" style="width:80px;">
|
<input type="number" name="quantity" value="{{ qty }}" min="1" step="0.01" class="form-control form-control-sm me-2" style="width:80px;">
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="submit">Aktualisieren</button>
|
<button class="btn btn-sm btn-outline-secondary" type="submit">Aktualisieren</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
{{ item.quantity | format_number }}
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">{{ item.product.price | format_number }} €</td>
|
<td class="text-end">{{ item.product.price | format_number }} €</td>
|
||||||
<td class="text-end">{{ item.total_amount | format_number }} €</td>
|
<td class="text-end">{{ item.total_amount | format_number }} €</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<a href="/cart/remove/{{ item.id }}" class="btn btn-sm btn-outline-danger">Entfernen</a>
|
{% if is_cart %}<a href="/cart/remove/{{ item.id }}" class="btn btn-sm btn-outline-danger">Entfernen</a>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -50,13 +54,14 @@
|
|||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="text-end fw-semibold">Gesamtsumme</td>
|
<td colspan="3" class="text-end fw-semibold">Gesamtsumme</td>
|
||||||
<td class="text-end fw-bold">{{ user.shopping_cart.total_amount | format_number }} €</td>
|
<td class="text-end fw-bold">{{ order.total_amount | format_number }} €</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if is_cart %}
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
<div>
|
<div>
|
||||||
<a href="/cart/clear" class="btn btn-outline-danger me-2">Warenkorb leeren</a>
|
<a href="/cart/clear" class="btn btn-outline-danger me-2">Warenkorb leeren</a>
|
||||||
@@ -64,6 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ UNITS_OF_MEASURE = {"piece": "Stück"}
|
|||||||
|
|
||||||
def format_number(value: float):
|
def format_number(value: float):
|
||||||
try:
|
try:
|
||||||
return f"{value:n}"
|
return f"{value:.2f}".replace(".", ",")
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -25,4 +25,5 @@ def get_jinja_renderer() -> Jinja2Templates:
|
|||||||
renderer.env.filters["transaction_type_de"] = lambda x: TRANSACTION_TYPE_DE[x]
|
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["units_of_measure_de"] = lambda x: UNITS_OF_MEASURE.get(x, x)
|
||||||
renderer.env.filters["format_number"] = format_number
|
renderer.env.filters["format_number"] = format_number
|
||||||
|
renderer.env.filters["timestamp_de"] = lambda x: x.strftime("%d.%m.%Y %H:%M")
|
||||||
return renderer
|
return renderer
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
from fake_data import fake
|
from fake_data import fake
|
||||||
from starlette.testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
@@ -29,14 +31,33 @@ def create_user_with_account(test_db, username: str, balance: float | None = Non
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def test_add_item_to_cart(client: TestClient, test_db):
|
def add_finalized_order_to_user(test_db, user, product) -> Order:
|
||||||
|
order = Order(user=user)
|
||||||
|
total_amount = product.price
|
||||||
|
order.items.append(
|
||||||
|
OrderItem(product=product, quantity=1, total_amount=total_amount)
|
||||||
|
)
|
||||||
|
order.transaction = Transaction(total_amount=total_amount, type="order")
|
||||||
|
order.transaction.account = user.accounts[0]
|
||||||
|
test_db.add(order)
|
||||||
|
test_db.flush()
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def product(test_db):
|
||||||
area = Area(**fake.area())
|
area = Area(**fake.area())
|
||||||
test_db.add(area)
|
test_db.add(area)
|
||||||
product = Product(**fake.product())
|
product = Product(**fake.product())
|
||||||
product.area = area
|
product.area = area
|
||||||
test_db.add(product)
|
test_db.add(product)
|
||||||
test_db.flush()
|
test_db.flush()
|
||||||
form_data = {"product_id": product.id, "quantity": 2, "area_id": area.id}
|
return product
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_item_to_cart(client: TestClient, test_db, product):
|
||||||
|
|
||||||
|
form_data = {"product_id": product.id, "quantity": 2, "area_id": product.area.id}
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/shop/cart/add",
|
"/shop/cart/add",
|
||||||
@@ -47,13 +68,7 @@ def test_add_item_to_cart(client: TestClient, test_db):
|
|||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
|
||||||
def test_finalize_order(client: TestClient, test_db):
|
def test_finalize_order(client: TestClient, test_db, product):
|
||||||
area = Area(**fake.area())
|
|
||||||
test_db.add(area)
|
|
||||||
product = Product(**fake.product())
|
|
||||||
product.area = area
|
|
||||||
test_db.add(product)
|
|
||||||
test_db.flush()
|
|
||||||
|
|
||||||
user = create_user_with_account(test_db, "test", balance=100.0)
|
user = create_user_with_account(test_db, "test", balance=100.0)
|
||||||
|
|
||||||
@@ -69,3 +84,24 @@ def test_finalize_order(client: TestClient, test_db):
|
|||||||
assert len(user.orders) == 2 # shopping cart + finalized order
|
assert len(user.orders) == 2 # shopping cart + finalized order
|
||||||
|
|
||||||
assert user.accounts[0].balance == Decimal(100.0) - (product.price * 2)
|
assert user.accounts[0].balance == Decimal(100.0) - (product.price * 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_view_order(client: TestClient, test_db, product):
|
||||||
|
user = create_user_with_account(test_db, "test")
|
||||||
|
|
||||||
|
order = add_finalized_order_to_user(test_db, user, product)
|
||||||
|
|
||||||
|
response = client.get(f"/shop/order/{order.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert f"Einkauf #{order.id}" in response.text
|
||||||
|
assert product.name in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_view_order_wrong_user(client: TestClient, test_db, product):
|
||||||
|
user = create_user_with_account(test_db, "test")
|
||||||
|
|
||||||
|
order = add_finalized_order_to_user(test_db, user, product)
|
||||||
|
|
||||||
|
with mock.patch.dict("os.environ", {"APS_username": "other_user"}):
|
||||||
|
response = client.get(f"/shop/order/{order.id}")
|
||||||
|
assert response.status_code == 403
|
||||||
|
|||||||
Reference in New Issue
Block a user