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

@@ -14,11 +14,11 @@ damit auch keine weitere Stelle, an der etwas kaputtgehen kann.
import logging
from datetime import timedelta
from sqlalchemy import delete, select
from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session
from app.config import settings
from app.mail import send_deactivation_notice
from app.mail import send_deactivation_notice, send_list_deletion_warning
from app.models import (
AppliedOp,
Article,
@@ -29,6 +29,7 @@ from app.models import (
EmailToken,
ListInvite,
ListItem,
ListMember,
Market,
ProductCache,
PublicShare,
@@ -142,6 +143,132 @@ def run_cleanup(db: Session) -> dict[str, int]:
counts.update(_retire_users(db))
db.commit()
counts.update(_archive_lists(db))
db.commit()
return counts
def _archive_lists(db: Session) -> dict[str, int]:
"""Archiviert lange nicht geöffnete Listen und löscht lange
archivierte.
Zwei Besonderheiten, die diesen Ablauf von der Kontenbereinigung
unterscheiden:
* Archiviert wird JE MITGLIED. Dass eine Person eine gemeinsame
Liste seit einem Jahr nicht geöffnet hat, sagt nichts über die
anderen - sie verschwindet nur aus ihrer eigenen Übersicht.
* Gelöscht wird nur nach dem Archivdatum des EIGENTÜMERS. Sonst
könnte ein eingeladenes Mitglied, das die Liste weggeräumt hat,
eine Löschung auslösen, die den Eigentümer trifft.
"""
now = utcnow()
counts: dict[str, int] = {}
archive_months = months_setting(db, "auto_archive_months", 12)
delete_months = months_setting(db, "archive_delete_months", 12)
# --- Automatisch archivieren, je Mitgliedschaft ---
if archive_months:
cutoff = now - timedelta(days=archive_months * 30)
stale = db.scalars(
select(ListMember)
.join(ShoppingList, ShoppingList.id == ListMember.list_id)
.where(
ListMember.archived_at.is_(None),
ListMember.last_opened_at.is_not(None),
ListMember.last_opened_at < cutoff,
ShoppingList.deleted_at.is_(None),
)
).all()
for member in stale:
member.archived_at = now
if stale:
counts["archivierte Listen"] = len(stale)
if not delete_months:
return counts
# --- Vorwarnen und löschen, nach dem Archivdatum des Eigentümers ---
delete_after = timedelta(days=delete_months * 30)
owners = db.scalars(
select(ListMember)
.join(ShoppingList, ShoppingList.id == ListMember.list_id)
.where(
ListMember.archived_at.is_not(None),
ShoppingList.deleted_at.is_(None),
ShoppingList.owner_id == ListMember.user_id,
)
).all()
warned = 0
deleted = 0
for member in owners:
lst = db.get(ShoppingList, member.list_id)
if lst is None or lst.deleted_at is not None:
continue
due = member.archived_at + delete_after
remaining = due - now
# Wer die Liste zwischenzeitlich wieder geöffnet hat, soll sie
# nicht verlieren - auch wenn das Archivdatum alt ist.
if member.last_opened_at and member.last_opened_at > now - delete_after:
continue
if remaining.total_seconds() <= 0:
log.info(
"Lösche Liste %s (%s) - seit %s im Archiv des Eigentümers",
lst.id, lst.name, member.archived_at.date(),
)
# Soft Delete: Offline gebliebene Geräte sollen noch
# erfahren, dass die Liste weg ist. Endgültig entfernt sie
# der Durchlauf nach CLEANUP_DELETED_DAYS.
lst.deleted_at = now
lst.rev += 1
deleted += 1
continue
stage = None
if remaining <= timedelta(days=1) and lst.warn_1d_at is None:
stage = 1
elif remaining <= timedelta(days=7) and lst.warn_7d_at is None:
stage = 7
if stage is None:
continue
owner = db.get(User, lst.owner_id)
if owner is None or not owner.is_active:
continue
item_count = db.scalar(
select(func.count())
.select_from(ListItem)
.where(ListItem.list_id == lst.id, ListItem.deleted_at.is_(None))
) or 0
send_list_deletion_warning(
owner.email, lst.name, stage,
member.archived_at.strftime("%d.%m.%Y"), item_count,
)
if stage == 1:
lst.warn_1d_at = now
# Falls die Sieben-Tage-Stufe übersprungen wurde, etwa weil
# die Frist verkürzt wurde: als erledigt vermerken.
lst.warn_7d_at = lst.warn_7d_at or now
else:
lst.warn_7d_at = now
warned += 1
if warned:
counts["Vorwarnungen zu Listen"] = warned
if deleted:
counts["gelöschte Listen"] = deleted
return counts