| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- """What of an archive a printer-scoped caller sees and may change (#1727, archive redesign).
- An archive is a file; each print of it is a run, and plates of one archive
- can print on several printers. A caller limited to some printers sees an
- archive when it, or any of its runs, is on one of theirs, and then only
- their prints of it: the outcome, video and finish photos of a run on a
- printer they can't see stay hidden. What changes the whole archive needs
- every printer it printed on.
- """
- from __future__ import annotations
- from dataclasses import dataclass, field
- from datetime import datetime
- from fastapi import HTTPException, status
- from sqlalchemy import select
- from sqlalchemy.ext.asyncio import AsyncSession
- from backend.app.core.printer_scope import PrinterScope
- from backend.app.models.print_log import INCLUDE_OPEN_RUNS, PrintLogEntry
- # Finish photos are named so (main._background_finish_photo, camera); a
- # photo uploaded by hand is not, nor one carried over from the library.
- _FINISH_PHOTO_PREFIX = "finish_"
- @dataclass(frozen=True)
- class ScopedView:
- """What a scoped caller sees of one archive beyond what it holds.
- ``run`` is their latest print of it, shown instead of the archive's
- latest when that one is on a printer outside their scope. The photos
- and the video of prints outside it are left out.
- """
- run: PrintLogEntry | None = None
- # The print whose figures (energy, wear) it shows: ``run``, or the
- # archive's latest when that is in the scope.
- shown: PrintLogEntry | None = None
- hidden_photos: frozenset[str] = field(default_factory=frozenset)
- # The archive's video, when it is of a print outside the scope.
- hidden_timelapse: str | None = None
- async def runs_by_archive(db: AsyncSession, archive_ids: list[int]) -> dict[int, list[PrintLogEntry]]:
- """Every run of each archive, a printing one included."""
- if not archive_ids:
- return {}
- rows = await db.scalars(
- select(PrintLogEntry)
- .where(PrintLogEntry.archive_id.in_(archive_ids))
- .execution_options(**{INCLUDE_OPEN_RUNS: True})
- )
- runs: dict[int, list[PrintLogEntry]] = {}
- for run in rows.all():
- if run.archive_id is not None:
- runs.setdefault(run.archive_id, []).append(run)
- return runs
- def _on_printer_in(scope: PrinterScope, run: PrintLogEntry) -> bool:
- """A print on a printer of the scope; one whose printer was deleted is no one's."""
- return run.printer_id is not None and scope.allows(run.printer_id)
- def view_of(archive, runs: list[PrintLogEntry], scope: PrinterScope) -> ScopedView | None:
- """What ``scope`` sees of ``archive`` given its ``runs``; None when it sees all of it."""
- if scope.is_unrestricted:
- return None
- inside = [r for r in runs if scope.allows(r.printer_id)]
- outside = [r for r in runs if not scope.allows(r.printer_id)]
- archive_inside = scope.allows(archive.printer_id)
- if archive_inside and not outside:
- return None
- shown = {p for r in inside for p in (r.photos or [])}
- hidden = {p for r in outside for p in (r.photos or []) if p not in shown}
- if not archive_inside:
- # A finish photo on no run's list is of a print logged before runs
- # kept their own: the archive's latest, here on a printer outside.
- on_runs = {p for r in runs for p in (r.photos or [])}
- hidden |= {p for p in archive.photos or [] if p not in on_runs and p.startswith(_FINISH_PHOTO_PREFIX)}
- hidden_timelapse = None
- if archive.timelapse_path:
- holders = [r for r in runs if r.timelapse_path == archive.timelapse_path]
- if not any(scope.allows(r.printer_id) for r in holders) if holders else not archive_inside:
- hidden_timelapse = archive.timelapse_path
- def latest(of: list[PrintLogEntry]) -> PrintLogEntry:
- return max(of, key=lambda r: (r.started_at is not None, r.started_at or r.created_at or datetime.min, r.id))
- run = shown = None
- if not archive_inside:
- theirs = [r for r in runs if _on_printer_in(scope, r)]
- if theirs:
- run = shown = latest(theirs)
- elif runs and scope.allows((newest := latest(runs)).printer_id):
- shown = newest
- return ScopedView(run=run, shown=shown, hidden_photos=frozenset(hidden), hidden_timelapse=hidden_timelapse)
- async def scoped_views(db: AsyncSession, archives, scope: PrinterScope) -> dict[int, ScopedView]:
- """``view_of`` for each archive that ``scope`` doesn't see whole."""
- if scope.is_unrestricted:
- return {}
- archives = list(archives)
- runs = await runs_by_archive(db, [a.id for a in archives])
- views = {}
- for archive in archives:
- view = view_of(archive, runs.get(archive.id, []), scope)
- if view is not None:
- views[archive.id] = view
- return views
- async def scoped_view(db: AsyncSession, archive, scope: PrinterScope) -> ScopedView | None:
- return (await scoped_views(db, [archive], scope)).get(archive.id)
- async def printed_in_scope(db: AsyncSession, archive, scope: PrinterScope) -> bool:
- """Whether ``scope`` sees ``archive``: on its own printer, or a run on one of theirs."""
- if scope.allows(archive.printer_id):
- return True
- runs = await runs_by_archive(db, [archive.id])
- return any(_on_printer_in(scope, r) for r in runs.get(archive.id, []))
- async def ensure_whole_archive_in_scope(db: AsyncSession, archive, scope: PrinterScope) -> None:
- """Refuse a change to the whole archive (its file, its video, the archive
- itself) unless ``scope`` sees every printer it printed on: it would change
- or remove those prints too. The caller sees the archive, so this says why
- (403) rather than hiding it."""
- if scope.is_unrestricted:
- return
- runs = (await runs_by_archive(db, [archive.id])).get(archive.id, [])
- if scope.allows(archive.printer_id) and all(scope.allows(r.printer_id) for r in runs):
- return
- raise HTTPException(
- status_code=status.HTTP_403_FORBIDDEN,
- detail="This archive was also printed on printers outside your access, so only someone "
- "who can see all of them can change or remove it",
- )
|