Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): integrate sftkit #248

Merged
merged 2 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
ABRECHNUNG_SERVICE__URL=http://localhost:8080
ABRECHNUNG_SERVICE__API_URL=https://localhost:8080/api
ABRECHNUNG_SERVICE__NAME=Abrechnung
ABRECHNUNG_DATABASE__HOST=abrechnung_postgres
ABRECHNUNG_DATABASE__USER=abrechnung
ABRECHNUNG_DATABASE__DBNAME=abrechnung
ABRECHNUNG_DATABASE__PASSWORD=replaceme # e.g. pwgen -s 64 1

ABRECHNUNG_API__SECRET_KEY=replaceme # pwgen -s 64 1
ABRECHNUNG_SERVICE__BASE_URL=https://localhost:8080
ABRECHNUNG_API__PORT=8080
ABRECHNUNG_API__ID=default

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/backend.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ jobs:
strategy:
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
os: [ubuntu-latest]
services:
postgres:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

[Compare the full difference.](https://github.com/SFTtech/abrechnung/compare/v0.14.0...HEAD)

**BREAKING**
- changed config structure to only include the reverse-proxy base_url once in the `api` section
- drop python 3.10 support

**OTHER**
- add Spanish and Tamil as supported languages
- rework group settings to just be a single page
- allow archiving of groups
Expand Down
5 changes: 3 additions & 2 deletions abrechnung/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from abrechnung.application.users import UserService
from abrechnung.config import Config
from abrechnung.framework.database import create_db_pool
from abrechnung.database.migrations import get_database

logger = logging.getLogger(__name__)

Expand All @@ -16,7 +16,8 @@ async def create_user(config: Config, name: str, email: str, skip_email_check: b
print("Passwords do not match!")
return

db_pool = await create_db_pool(config.database)
database = get_database(config.database)
db_pool = await database.create_pool()
user_service = UserService(db_pool, config)
user_service.enable_registration = True
if skip_email_check:
Expand Down
36 changes: 19 additions & 17 deletions abrechnung/application/accounts.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from typing import Optional, Union

import asyncpg
from sftkit.database import Connection
from sftkit.error import InvalidArgument
from sftkit.service import Service, with_db_connection, with_db_transaction

from abrechnung.config import Config
from abrechnung.core.auth import check_group_permissions, create_group_log
from abrechnung.core.decorators import (
requires_group_permissions,
with_group_last_changed_update,
)
from abrechnung.core.errors import InvalidCommand, NotFoundError
from abrechnung.core.service import Service
from abrechnung.domain.accounts import (
Account,
AccountType,
Expand All @@ -18,13 +20,11 @@
)
from abrechnung.domain.groups import GroupMember
from abrechnung.domain.users import User
from abrechnung.framework.database import Connection
from abrechnung.framework.decorators import with_db_connection, with_db_transaction

from .common import _get_or_create_tag_ids


class AccountService(Service):
class AccountService(Service[Config]):
@staticmethod
async def _commit_revision(conn: asyncpg.Connection, revision_id: int):
"""Touches a revision to have all associated constraints run"""
Expand Down Expand Up @@ -57,15 +57,17 @@ async def _check_account_permissions(
account_id,
)
if not result:
raise NotFoundError(f"account not found")
raise InvalidArgument(f"account not found")

if can_write and not (result["can_write"] or result["is_owner"]):
raise PermissionError(f"user does not have write permissions")

if account_type:
type_check = [account_type] if isinstance(account_type, str) else account_type
if result["type"] not in type_check:
raise InvalidCommand(f"Transaction type {result['type']} does not match the expected type {type_check}")
raise InvalidArgument(
f"Transaction type {result['type']} does not match the expected type {type_check}"
)

return result["group_id"], result["type"]

Expand All @@ -91,7 +93,7 @@ async def _get_or_create_pending_account_change(self, conn: asyncpg.Connection,
)

if last_committed_revision is None:
raise InvalidCommand(f"Cannot edit account {account_id} as it has no committed changes.")
raise InvalidArgument(f"Cannot edit account {account_id} as it has no committed changes.")

# copy all existing transaction data into a new history entry
await conn.execute(
Expand Down Expand Up @@ -124,7 +126,7 @@ async def _check_account_exists(conn: asyncpg.Connection, group_id: int, account
account_id,
)
if not acc:
raise NotFoundError(f"Account with id {account_id}")
raise InvalidArgument(f"Account with id {account_id}")

return True

Expand Down Expand Up @@ -210,7 +212,7 @@ async def create_account(
group_membership: GroupMember,
) -> int:
if account.clearing_shares and account.type != AccountType.clearing:
raise InvalidCommand(
raise InvalidArgument(
f"'{account.type.value}' accounts cannot have associated settlement distribution shares"
)

Expand Down Expand Up @@ -279,7 +281,7 @@ async def update_account(
)
membership = await check_group_permissions(conn=conn, group_id=group_id, user=user, can_write=True)
if account.clearing_shares and account_type != AccountType.clearing.value:
raise InvalidCommand(f"'{account_type}' accounts cannot have associated settlement distribution shares")
raise InvalidArgument(f"'{account_type}' accounts cannot have associated settlement distribution shares")

revision_id = await self._create_revision(conn=conn, user=user, account_id=account_id)

Expand Down Expand Up @@ -350,7 +352,7 @@ async def delete_account(
account_id,
)
if row is None:
raise InvalidCommand(f"Account does not exist")
raise InvalidArgument(f"Account does not exist")

# TODO: FIXME move this check into the database

Expand All @@ -374,28 +376,28 @@ async def delete_account(
)

if has_shares or has_usages:
raise InvalidCommand(f"Cannot delete an account that is references by a transaction")
raise InvalidArgument(f"Cannot delete an account that is references by a transaction")

if has_clearing_shares:
raise InvalidCommand(f"Cannot delete an account that is references by a clearing account")
raise InvalidArgument(f"Cannot delete an account that is references by a clearing account")

row = await conn.fetchrow(
"select name, revision_id, deleted from account_state_valid_at() where account_id = $1",
account_id,
)
if row is None:
raise InvalidCommand(f"Cannot delete an account without any committed changes")
raise InvalidArgument(f"Cannot delete an account without any committed changes")

if row["deleted"]:
raise InvalidCommand(f"Cannot delete an already deleted account")
raise InvalidArgument(f"Cannot delete an already deleted account")

has_clearing_shares = await conn.fetchval(
"select exists (select from account_state_valid_at() p where not p.deleted and $1 = any(p.involved_accounts))",
account_id,
)

if has_clearing_shares:
raise InvalidCommand(f"Cannot delete an account that is references by another clearing account")
raise InvalidArgument(f"Cannot delete an account that is references by another clearing account")

revision_id = await conn.fetchval(
"insert into account_revision (user_id, account_id) values ($1, $2) returning id",
Expand Down
4 changes: 2 additions & 2 deletions abrechnung/application/common.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import Optional

import asyncpg
from sftkit.database import Connection


async def _get_or_create_tag_ids(*, conn: asyncpg.Connection, group_id: int, tags: Optional[list[str]]) -> list[int]:
async def _get_or_create_tag_ids(*, conn: Connection, group_id: int, tags: Optional[list[str]]) -> list[int]:
if not tags or len(tags) <= 0:
return []
tag_rows = await conn.fetch(
Expand Down
20 changes: 10 additions & 10 deletions abrechnung/application/groups.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from datetime import datetime

import asyncpg
from sftkit.database import Connection
from sftkit.error import InvalidArgument
from sftkit.service import Service, with_db_transaction

from abrechnung.config import Config
from abrechnung.core.auth import create_group_log
from abrechnung.core.decorators import (
requires_group_permissions,
with_group_last_changed_update,
)
from abrechnung.core.errors import InvalidCommand, NotFoundError
from abrechnung.core.service import Service
from abrechnung.domain.accounts import AccountType
from abrechnung.domain.groups import (
Group,
Expand All @@ -18,11 +20,9 @@
GroupPreview,
)
from abrechnung.domain.users import User
from abrechnung.framework.database import Connection
from abrechnung.framework.decorators import with_db_transaction


class GroupService(Service):
class GroupService(Service[Config]):
@with_db_transaction
async def create_group(
self,
Expand Down Expand Up @@ -116,7 +116,7 @@ async def delete_invite(
group_id,
)
if not deleted_id:
raise NotFoundError(f"No invite with the given id exists")
raise InvalidArgument(f"No invite with the given id exists")
await create_group_log(conn=conn, group_id=group_id, user=user, type="invite-deleted")

async def _create_user_account(self, conn: asyncpg.Connection, group_id: int, user: User) -> int:
Expand Down Expand Up @@ -165,7 +165,7 @@ async def join_group(self, *, conn: Connection, user: User, invite_token: str) -
invite["group_id"],
)
if user_is_already_member:
raise InvalidCommand(f"User is already a member of this group")
raise InvalidArgument(f"User is already a member of this group")

await conn.execute(
"insert into group_membership (user_id, group_id, invited_by, can_write, is_owner) "
Expand Down Expand Up @@ -249,7 +249,7 @@ async def update_member_permissions(
is_owner: bool,
):
if user.id == member_id:
raise InvalidCommand(f"group members cannot modify their own privileges")
raise InvalidArgument(f"group members cannot modify their own privileges")

# not possible to have an owner without can_write
can_write = can_write if not is_owner else True
Expand All @@ -260,7 +260,7 @@ async def update_member_permissions(
member_id,
)
if membership is None:
raise NotFoundError(f"member with id {member_id} does not exist")
raise InvalidArgument(f"member with id {member_id} does not exist")

if membership["is_owner"] == is_owner and membership["can_write"] == can_write: # no changes
return
Expand Down Expand Up @@ -330,7 +330,7 @@ async def delete_group(self, *, conn: Connection, user: User, group_id: int):
group_id,
)
if n_members != 1:
raise PermissionError(f"Can only delete a group when you are the last member")
raise InvalidArgument(f"Can only delete a group when you are the last member")

await conn.execute("delete from grp where id = $1", group_id)

Expand Down
Loading
Loading