accounts: Add new accounts and add users and balance to accounts

Close #5
This commit is contained in:
2026-02-14 10:54:40 +01:00
parent f8c2727226
commit 8daf064e4d
3 changed files with 215 additions and 5 deletions

View File

@@ -2,6 +2,7 @@ from typing import Annotated
from fastapi import APIRouter, File, Form, HTTPException, Request from fastapi import APIRouter, File, Form, HTTPException, Request
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from starlette import status from starlette import status
from starlette.responses import RedirectResponse from starlette.responses import RedirectResponse
@@ -12,6 +13,7 @@ from allmende_payment_system.models import (
Area, Area,
Permission, Permission,
Product, Product,
Transaction,
User, User,
UserGroup, UserGroup,
) )
@@ -296,8 +298,78 @@ async def get_accounts(
templates = get_jinja_renderer() templates = get_jinja_renderer()
accounts = session.scalars(select(Account)).all() accounts = session.scalars(select(Account)).all()
users = session.scalars(select(User)).all()
return templates.TemplateResponse( return templates.TemplateResponse(
"accounts.html.jinja", "accounts.html.jinja",
context={"request": request, "accounts": accounts}, context={"request": request, "accounts": accounts, "users": users},
)
@admin_router.post("/accounts/new")
async def create_account(
request: Request,
session: SessionDep,
user: UserDep,
account_name: Annotated[str, Form()],
):
if not user.has_permission("account", "edit"):
raise HTTPException(status_code=403, detail="Insufficient permissions")
account = Account(name=account_name)
session.add(account)
try:
session.flush()
except IntegrityError as e:
session.rollback()
raise HTTPException(
status_code=400, detail="Account with this name already exists"
) from e
return RedirectResponse(
url="/admin/accounts", status_code=status.HTTP_303_SEE_OTHER
)
@admin_router.post("/accounts/{account_id}/add_user")
async def add_user_to_account(
request: Request,
session: SessionDep,
user: UserDep,
account_id: int,
user_id: Annotated[int, Form()],
):
if not user.has_permission("account", "edit"):
raise HTTPException(status_code=403, detail="Insufficient permissions")
account = session.execute(
select(Account).where(Account.id == account_id)
).scalar_one()
user_to_add = session.execute(select(User).where(User.id == user_id)).scalar_one()
account.users.append(user_to_add)
return RedirectResponse(
url="/admin/accounts", status_code=status.HTTP_303_SEE_OTHER
)
@admin_router.post("/accounts/{account_id}/add_balance")
async def add_balance_to_account(
request: Request,
session: SessionDep,
user: UserDep,
account_id: int,
amount: Annotated[float, Form()],
):
if not user.has_permission("account", "edit"):
raise HTTPException(status_code=403, detail="Insufficient permissions")
account = session.execute(
select(Account).where(Account.id == account_id)
).scalar_one()
account.transactions.append(Transaction(type="deposit", total_amount=amount))
return RedirectResponse(
url="/admin/accounts", status_code=status.HTTP_303_SEE_OTHER
) )

View File

@@ -2,7 +2,30 @@
{% block content %} {% block content %}
<div class="mb-4"> <div class="mb-4">
<h2 class="h4 mb-3">Konten verwalten</h2> <h2 class="h4 mb-3">Konten verwalten</h2>
<a class="btn btn-primary" href="/admin/accounts/new">Neues Konto erstellen</a> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newAccountModal">Neues Konto erstellen</button>
</div>
<div class="modal fade" id="newAccountModal" tabindex="-1" aria-labelledby="newAccountModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/admin/accounts/new">
<div class="modal-header">
<h5 class="modal-title" id="newAccountModalLabel">Neues Konto erstellen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="account_name" class="form-label">Name</label>
<input type="text" class="form-control" id="account_name" name="account_name" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Erstellen</button>
</div>
</form>
</div>
</div>
</div> </div>
{% if accounts|length == 0 %} {% if accounts|length == 0 %}
@@ -24,7 +47,63 @@
<td>{{ account.name }}</td> <td>{{ account.name }}</td>
<td>{{ account.users | map(attribute='display_name') | join(", ") }}</td> <td>{{ account.users | map(attribute='display_name') | join(", ") }}</td>
<td>{{ account.balance | format_number }} €</td> <td>{{ account.balance | format_number }} €</td>
<td> actions </td> <td>
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addUserModal-{{ account.id }}">Benutzer hinzufügen</button>
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addBalanceModal-{{ account.id }}">Guthaben hinzufügen</button>
{# ADD USER #}
<div class="modal fade" id="addUserModal-{{ account.id }}" tabindex="-1" aria-labelledby="addUserModalLabel-{{ account.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/admin/accounts/{{ account.id }}/add_user">
<div class="modal-header">
<h5 class="modal-title" id="addUserModalLabel-{{ account.id }}">Benutzer zu {{ account.name }} hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="select-user-{{ account.id }}" class="form-label">Benutzer</label>
<select class="form-select" id="select-user-{{ account.id }}" name="user_id" required>
<option value="">-- auswählen --</option>
{% for user in users if user not in account.users %}
<option value="{{ user.id }}">{{ user.display_name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
</div>
</div>
</div>
{# ADD BALANCE #}
<div class="modal fade" id="addBalanceModal-{{ account.id }}" tabindex="-1" aria-labelledby="addBalanceModalLabel-{{ account.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/admin/accounts/{{ account.id }}/add_balance">
<div class="modal-header">
<h5 class="modal-title" id="addBalanceModalLabel-{{ account.id }}">Guthaben zu {{ account.name }} hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="amount" class="form-label">Betrag in €</label>
<input type="number" min="0.01" step="0.01" class="form-control" id="amount" name="amount" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
</div>
</div>
</div>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -5,7 +5,7 @@ from sqlalchemy.orm import Session
from allmende_payment_system.app import app from allmende_payment_system.app import app
from allmende_payment_system.database import ensure_user from allmende_payment_system.database import ensure_user
from allmende_payment_system.models import Permission, User, UserGroup from allmende_payment_system.models import Account, Permission, User, UserGroup
@pytest.fixture @pytest.fixture
@@ -15,6 +15,7 @@ def admin_user(test_db):
group = UserGroup(id=1, name="Admins") group = UserGroup(id=1, name="Admins")
group.permissions.append(Permission(scope="user", action="edit")) group.permissions.append(Permission(scope="user", action="edit"))
group.permissions.append(Permission(scope="account", action="edit"))
user.user_groups.append(group) user.user_groups.append(group)
test_db.add(group) test_db.add(group)
test_db.flush() test_db.flush()
@@ -97,6 +98,7 @@ def test_group_add_permission_illegal_format(test_db, client, admin_user):
def test_group_remove_permission(test_db, client, admin_user): def test_group_remove_permission(test_db, client, admin_user):
group = test_db.query(UserGroup).scalar() group = test_db.query(UserGroup).scalar()
num_permissions_before = len(group.permissions)
response = client.get( response = client.get(
f"/admin/groups/{group.id}/remove_permission/1", f"/admin/groups/{group.id}/remove_permission/1",
user=admin_user, user=admin_user,
@@ -104,7 +106,7 @@ def test_group_remove_permission(test_db, client, admin_user):
) )
assert response.status_code == 303 assert response.status_code == 303
group = test_db.execute(select(UserGroup).where(UserGroup.id == group.id)).scalar() group = test_db.execute(select(UserGroup).where(UserGroup.id == group.id)).scalar()
assert 0 == len(group.permissions) assert num_permissions_before - 1 == len(group.permissions)
def test_create_group(test_db, client, admin_user): def test_create_group(test_db, client, admin_user):
@@ -128,3 +130,60 @@ def test_delete_group(test_db, client, admin_user):
test_db.execute(select(UserGroup).where(UserGroup.id == group.id)).scalar() test_db.execute(select(UserGroup).where(UserGroup.id == group.id)).scalar()
is None is None
) )
def test_create_account(test_db, client, admin_user):
response = client.post(
"/admin/accounts/new",
data={"account_name": "New Account"},
user=admin_user,
follow_redirects=False,
)
assert response.status_code == 303
assert test_db.query(Account).filter_by(name="New Account").scalar() is not None
# try to create another account with the same name, should fail
response = client.post(
"/admin/accounts/new",
data={"account_name": "New Account"},
user=admin_user,
follow_redirects=False,
)
assert response.status_code == 400
def test_add_user_to_account(test_db, client, admin_user):
user_info = {"username": "test", "display_name": "Display Test"}
user = ensure_user(user_info, test_db)
account = Account(name="Test Account")
test_db.add(account)
test_db.flush()
response = client.post(
f"/admin/accounts/{account.id}/add_user",
data={"user_id": user.id},
user=admin_user,
follow_redirects=False,
)
assert response.status_code == 303
account = test_db.execute(select(Account).where(Account.id == account.id)).scalar()
assert any(u.username == "test" for u in account.users)
def test_add_balance_to_account(test_db, client, admin_user):
account = Account(name="Test Account")
test_db.add(account)
test_db.flush()
response = client.post(
f"/admin/accounts/{account.id}/add_balance",
data={"amount": "100.00"},
user=admin_user,
follow_redirects=False,
)
assert response.status_code == 303
account = test_db.execute(select(Account).where(Account.id == account.id)).scalar()
assert account.balance == 100.00