neue Archivfunktion und Optimierung an Artikelbearbeitung
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user