accounts: Add new accounts and add users and balance to accounts
Close #5
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user