mathly / mathly2 /cleanup.py
wzender's picture
feat(board-work): implement board work feature for student answers
aeda545
Raw History Blame Contribute Delete
3.65 kB
"""The hourly cleanup of pictures, and deleting a student's pictures on request. `python -m mathly2.cleanup` runs it once; the server runs it
every hour when it is connected to Supabase."""
from __future__ import annotations
import logging
import threading
from datetime import datetime
from . import clock, pictures, retention
log = logging.getLogger("mathly.cleanup")
def run(store, backend, now: datetime | None = None) -> dict:
"""Remove pictures nothing refers to (after a day), answer pictures past their school year, and the pages of removed assignments.
Returns how many of each were removed. A picture whose bytes cannot be deleted now is left for the next run."""
now = now or clock.now()
today = clock.school_day(now)
removed = {"orphans": 0, "expired": 0, "removed_assignments": 0}
for pic in store.pictures():
refs = store.picture_refs(pic["id"])
referenced = any(refs.values())
if pic["kind"] in ("answer", "work") and retention.is_expired(clock.school_day(pic["created_at"]), today):
reason = "expired"
elif not referenced and now - pic["created_at"] > retention.ORPHAN_AGE:
reason = "orphans"
elif pic["kind"] == "page" and refs["assignments"] and all((store.get_assignment(a) or {}).get("removed_at") for a in refs["assignments"]):
reason = "removed_assignments"
else:
continue
try:
pictures.remove_picture(store, backend, pic, now)
removed[reason] += 1
except pictures.PictureError as exc:
log.warning("could not delete a picture (%s); it stays for the next run", exc.code)
return removed
def delete_answer_pictures(store, backend, assignment_ids, user_id: str, now: datetime | None = None) -> int:
"""Delete a student's answer pictures for these assignments now (a teacher of the class, a manager or a developer asked). The submissions
keep their grade and note. Returns how many pictures were deleted."""
count = 0
for sub in store.submissions_of_assignments(assignment_ids):
if sub["user_id"] != user_id:
continue
for pid in sub["pictures"]:
pic = store.get_picture(pid)
if pic and pic["kind"] == "answer" and pic["owner_id"] == user_id and not pic["deleted_at"]:
pictures.remove_picture(store, backend, pic, now)
count += 1
return count
def start_hourly(store, backend, every: float = 3600.0) -> threading.Event:
"""Run the cleanup now and then every `every` seconds in a background thread. Returns an event that stops it when set."""
stop = threading.Event()
def loop() -> None:
while True:
try:
log.info("cleanup removed %s", run(store, backend))
except Exception: # a failed run must not stop the next one
log.exception("the picture cleanup failed")
if stop.wait(every):
return
threading.Thread(target=loop, name="picture-cleanup", daemon=True).start()
return stop
def main() -> None:
from .config import load_settings
from .store import SupabaseStore
settings = load_settings()
if not settings.supabase_on:
raise SystemExit("Set the Supabase settings: the cleanup works on the real database.")
store = SupabaseStore(settings.supabase_url, settings.supabase_secret)
backend = pictures.SupabasePictures(settings.supabase_url, settings.supabase_secret, settings.answer_bucket)
print("Removed", run(store, backend))
if __name__ == "__main__":
main()