neue Archivfunktion und Optimierung an Artikelbearbeitung

This commit is contained in:
2026-08-09 18:24:04 +02:00
parent dd51532533
commit b80e2d5a6c
23 changed files with 1185 additions and 77 deletions

View File

@@ -1,4 +1,6 @@
from fastapi import APIRouter, HTTPException, status
from datetime import datetime, timedelta
from fastapi import APIRouter, HTTPException, Query, status
from sqlalchemy import func, select
from app.deps import DbSession, VerifiedUser
@@ -17,13 +19,38 @@ from app.schemas_shopping import (
MemberRoleIn,
)
from app.security import utcnow
from app.users import months_setting
router = APIRouter(prefix="/api/lists", tags=["lists"])
MAX_LISTS_PER_USER = 100
def _to_out(db, lst: ShoppingList, role: str, may_share: bool = False) -> ListOut:
ARCHIVE_DELETE_MONTHS_DEFAULT = 12
def _delete_due(db, lst: ShoppingList) -> datetime | None:
"""Wann würde die Liste automatisch gelöscht?
Maßgeblich ist ausschließlich das Archivdatum des EIGENTÜMERS. Dass
ein eingeladenes Mitglied die Liste weggeräumt hat, sagt nichts
darüber, ob sie noch gebraucht wird.
"""
owner = db.get(ListMember, (lst.id, lst.owner_id))
if owner is None or owner.archived_at is None:
return None
months = months_setting(
db, "archive_delete_months", ARCHIVE_DELETE_MONTHS_DEFAULT
)
if not months:
return None
return owner.archived_at + timedelta(days=months * 30)
def _to_out(
db, lst: ShoppingList, role: str, may_share: bool = False,
archived_at: datetime | None = None,
) -> ListOut:
count = db.scalar(
select(func.count()).select_from(ListMember).where(ListMember.list_id == lst.id)
)
@@ -31,18 +58,40 @@ def _to_out(db, lst: ShoppingList, role: str, may_share: bool = False) -> ListOu
id=lst.id, name=lst.name, owner_id=lst.owner_id, rev=lst.rev,
role=role, may_share_public=(role == "owner") or may_share,
member_count=count or 0, created_at=lst.created_at,
archived=archived_at is not None,
archived_at=archived_at,
delete_due_at=_delete_due(db, lst) if role == "owner" else None,
)
@router.get("", response_model=list[ListOut])
def my_lists(db: DbSession, user: VerifiedUser):
def my_lists(
db: DbSession,
user: VerifiedUser,
archived: bool | None = Query(
default=False,
description="False = nur aktive, True = nur archivierte, "
"nicht gesetzt = alle",
),
):
rows = db.execute(
select(ShoppingList, ListMember.role, ListMember.may_share_public)
select(
ShoppingList,
ListMember.role,
ListMember.may_share_public,
ListMember.archived_at,
)
.join(ListMember, ListMember.list_id == ShoppingList.id)
.where(ListMember.user_id == user.id, ShoppingList.deleted_at.is_(None))
.order_by(ShoppingList.created_at)
).all()
return [_to_out(db, lst, role, may) for lst, role, may in rows]
if archived is True:
rows = [r for r in rows if r[3] is not None]
elif archived is False:
rows = [r for r in rows if r[3] is None]
return [_to_out(db, lst, role, may, at) for lst, role, may, at in rows]
@router.post("", response_model=ListOut, status_code=status.HTTP_201_CREATED)
@@ -71,7 +120,40 @@ def create_list(payload: ListCreateIn, db: DbSession, user: VerifiedUser):
@router.get("/{list_id}", response_model=ListOut)
def read_list(lst: ReadableList, db: DbSession, user: VerifiedUser):
member = db.get(ListMember, (lst.id, user.id))
return _to_out(db, lst, member.role, member.may_share_public)
return _to_out(db, lst, member.role, member.may_share_public, member.archived_at)
@router.post("/{list_id}/archive", response_model=ListOut)
def archive_list(lst: ReadableList, db: DbSession, user: VerifiedUser):
"""Nimmt die Liste aus der eigenen Übersicht.
Wirkt nur für die aufrufende Person. Eine gemeinsame Liste
wegzuräumen, weil einer sie nicht mehr braucht, wäre ein Übergriff
auf die anderen.
"""
member = db.get(ListMember, (lst.id, user.id))
if member.archived_at is None:
member.archived_at = utcnow()
db.commit()
return _to_out(db, lst, member.role, member.may_share_public, member.archived_at)
@router.post("/{list_id}/unarchive", response_model=ListOut)
def unarchive_list(lst: ReadableList, db: DbSession, user: VerifiedUser):
member = db.get(ListMember, (lst.id, user.id))
member.archived_at = None
# Zähler zurücksetzen, sonst wandert die Liste beim nächsten
# Durchlauf sofort wieder ins Archiv.
member.last_opened_at = utcnow()
# Beim Eigentümer zusätzlich die Vorwarnungen zurücknehmen: Die Frist
# beginnt von vorn, also soll auch wieder gewarnt werden.
if lst.owner_id == user.id:
lst.warn_7d_at = None
lst.warn_1d_at = None
db.commit()
return _to_out(db, lst, member.role, member.may_share_public, None)
@router.get("/{list_id}/snapshot", response_model=ListSnapshot)
@@ -91,7 +173,7 @@ def snapshot(lst: ReadableList, db: DbSession, user: VerifiedUser):
).all()
return ListSnapshot(
list=_to_out(db, lst, member.role, member.may_share_public),
list=_to_out(db, lst, member.role, member.may_share_public, member.archived_at),
view=build_view(db, lst),
markets=[MarketOut.model_validate(m) for m in markets],
categories=[CategoryOut.model_validate(c) for c in categories],