Files
F-r-ldraledigplannerare/backend/app/repositories_impl/sqlite/user_repository.py
T
jacobh2 0e9b20ec37 Track preschool planning days, partner invites and real vacation forfeiture
Three strands of work that grew together across the same settings screen and
API client, so they land as one change.

Per-year preschool planning days ("studiedagar"): new table, repository
(SQLite + Postgres), service, router and migration 007, plus the hooks and
Settings UI to plan and record them per child and year. Allocation now
reserves the recorded figure for a year where one exists and only falls back
to the child-level estimate otherwise, marking that fallback as assumed.

Partner invitations: workspaces issue and list invitations, registration
accepts an invite token to join the inviter's workspace instead of creating a
personal one, and a /join/:token page plus the settings invite panel drive it.

Vacation forfeiture: tips and the dashboard now distinguish days a parent
genuinely loses from days that merely age, so the warning stops firing on
balances that were never at risk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 11:22:07 +02:00

107 lines
4.0 KiB
Python

"""SQLite implementation of UserRepository."""
from datetime import datetime
from uuid import UUID
import aiosqlite
from app.db.connection import commit
from app.entities.user import User
from app.repositories.filter_validator import validate_filters
from app.repositories.user_repository import UserRepository
from app.utils.errors import NotFoundError
class SQLiteUserRepository(UserRepository):
def __init__(self, connection: aiosqlite.Connection) -> None:
self._conn = connection
@staticmethod
def _row_to_entity(row: aiosqlite.Row) -> User:
return User(
id=UUID(row["id"]),
personal_data_uuid=UUID(row["personal_data_uuid"]),
email=row["email"],
hashed_password=row["hashed_password"],
created_at=datetime.fromisoformat(row["created_at"]),
is_active=bool(row["is_active"]),
)
async def create(self, item: User) -> User:
now = datetime.now()
item.created_at = item.created_at or now
await self._conn.execute(
"""
INSERT INTO "user"
(id, personal_data_uuid, email, hashed_password, created_at, is_active)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
str(item.id),
str(item.personal_data_uuid),
item.email,
item.hashed_password,
item.created_at.isoformat(),
int(item.is_active),
),
)
await commit(self._conn)
return item
async def get_by_id(self, id: UUID) -> User | None:
cursor = await self._conn.execute('SELECT * FROM "user" WHERE id = ?', (str(id),))
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def get_by_email(self, email: str) -> User | None:
cursor = await self._conn.execute('SELECT * FROM "user" WHERE email = ?', (email,))
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def get_by_personal_data_uuid(self, personal_data_uuid: UUID) -> User | None:
cursor = await self._conn.execute(
'SELECT * FROM "user" WHERE personal_data_uuid = ?', (str(personal_data_uuid),)
)
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def update(self, id: UUID, item: User) -> User:
# `personal_data_uuid` is included here (unlike a typical immutable
# FK) because `WorkspaceService.redeem_invitation` and
# `AuthService.register(..., invite_token=...)` re-point a user's
# login onto an existing placeholder `personal_data` row when
# claiming it. `user.personal_data_uuid` is UNIQUE, so re-pointing it
# onto a row already linked to another user fails at the DB.
cursor = await self._conn.execute(
"""
UPDATE "user"
SET personal_data_uuid = ?, email = ?, hashed_password = ?, is_active = ?
WHERE id = ?
""",
(
str(item.personal_data_uuid),
item.email,
item.hashed_password,
int(item.is_active),
str(id),
),
)
await commit(self._conn)
if cursor.rowcount == 0:
raise NotFoundError(f"User {id} not found")
refreshed = await self.get_by_id(id)
assert refreshed is not None
return refreshed
async def delete(self, id: UUID) -> None:
cursor = await self._conn.execute('DELETE FROM "user" WHERE id = ?', (str(id),))
await commit(self._conn)
if cursor.rowcount == 0:
raise NotFoundError(f"User {id} not found")
async def list(self, **filters: object) -> list[User]:
validate_filters(filters, set())
cursor = await self._conn.execute('SELECT * FROM "user"')
rows = await cursor.fetchall()
return [self._row_to_entity(r) for r in rows]