test_export_by_run.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. """The archive export has one row per print (archive redesign P3, decision 5).
  2. An archive is a file and each print of it a run: a row holds the file's
  3. columns and one run's own. An archive printed before runs were logged keeps
  4. its one row; the print filters pick prints, the others archives.
  5. """
  6. from __future__ import annotations
  7. import csv
  8. import io
  9. from datetime import datetime
  10. import pytest
  11. from backend.app.models.archive import PrintArchive
  12. from backend.app.models.print_log import PrintLogEntry
  13. from backend.tests.integration.test_archives_last_per_printer import _admin_token, _auth, _user
  14. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  15. T1 = datetime(2026, 10, 1, 9)
  16. T2 = datetime(2026, 10, 20, 9)
  17. async def _rows(async_client, headers=None, **params) -> list[dict]:
  18. response = await async_client.get(
  19. "/api/v1/archives/export", params={"format": "csv", **params}, headers=headers or {}
  20. )
  21. assert response.status_code == 200, response.text
  22. return list(csv.DictReader(io.StringIO(response.content.decode("utf-8-sig"))))
  23. @pytest.fixture
  24. async def prints(db_session, printer_factory):
  25. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  26. planter = PrintArchive(
  27. printer_id=b.id,
  28. filename="planter.3mf",
  29. file_path="archives/t/planter.3mf",
  30. file_size=1,
  31. print_name="Planter",
  32. status="failed",
  33. tags="garden",
  34. notes="two plates",
  35. created_at=T1,
  36. )
  37. legacy = PrintArchive(
  38. printer_id=a.id,
  39. filename="hook.3mf",
  40. file_path="archives/t/hook.3mf",
  41. file_size=1,
  42. print_name="Hook",
  43. status="completed",
  44. filament_used_grams=5.0,
  45. created_at=T1,
  46. )
  47. db_session.add_all([planter, legacy])
  48. await db_session.flush()
  49. first = PrintLogEntry(
  50. archive_id=planter.id,
  51. printer_id=a.id,
  52. printer_name="A",
  53. plate_id=1,
  54. status="completed",
  55. started_at=T1,
  56. filament_used_grams=12.0,
  57. user_verdict="good",
  58. )
  59. second = PrintLogEntry(
  60. archive_id=planter.id,
  61. printer_id=b.id,
  62. printer_name="B",
  63. plate_id=2,
  64. status="failed",
  65. started_at=T2,
  66. filament_used_grams=3.0,
  67. failure_reason="spaghetti",
  68. )
  69. db_session.add_all([first, second])
  70. await db_session.commit()
  71. return {"a": a.id, "b": b.id, "planter": planter.id, "legacy": legacy.id, "first": first.id, "second": second.id}
  72. async def test_one_row_per_print(async_client, prints):
  73. rows = await _rows(async_client)
  74. planter = [r for r in rows if r["Print Name"] == "Planter"]
  75. assert [(r["Print ID"], r["Printer"], r["Plate"], r["Status"], r["Filament (g)"]) for r in planter] == [
  76. (str(prints["second"]), "B", "2", "failed", "3.0"),
  77. (str(prints["first"]), "A", "1", "completed", "12.0"),
  78. ]
  79. # The file's columns on each.
  80. assert {(r["ID"], r["Tags"], r["Notes"]) for r in planter} == {(str(prints["planter"]), "garden", "two plates")}
  81. assert [r["Outcome"] for r in planter] == ["", "good"]
  82. async def test_a_print_logged_before_runs_keeps_its_row(async_client, prints):
  83. [hook] = [r for r in await _rows(async_client) if r["Print Name"] == "Hook"]
  84. assert (hook["Print ID"], hook["Status"], hook["Filament (g)"]) == ("", "completed", "5.0")
  85. async def test_the_print_filters_pick_prints(async_client, prints):
  86. on_a = await _rows(async_client, printer_id=prints["a"])
  87. failed = await _rows(async_client, status="failed")
  88. late = await _rows(async_client, date_from="2026-10-15")
  89. assert sorted(r["Print ID"] for r in on_a) == sorted(["", str(prints["first"])])
  90. assert [r["Print ID"] for r in failed] == [str(prints["second"])]
  91. assert [r["Print ID"] for r in late] == [str(prints["second"])]
  92. async def test_a_user_limited_to_some_printers_gets_their_prints(async_client, prints):
  93. admin = await _admin_token(async_client)
  94. jwt, _ = await _user(async_client, admin, "of_a", ["archives:read_all", "printers:read"], printer_ids=[prints["a"]])
  95. rows = await _rows(async_client, headers=_auth(jwt))
  96. planter = [r for r in rows if r["Print Name"] == "Planter"]
  97. assert [r["Print ID"] for r in planter] == [str(prints["first"])]
  98. async def test_a_file_printed_only_outside_their_printers_shows_no_print(async_client, db_session, prints):
  99. """Visible by its own printer (the archive names A), every print on B."""
  100. archive = await db_session.get(PrintArchive, prints["planter"])
  101. archive.printer_id = prints["a"]
  102. first = await db_session.get(PrintLogEntry, prints["first"])
  103. first.printer_id = prints["b"]
  104. await db_session.commit()
  105. admin = await _admin_token(async_client)
  106. jwt, _ = await _user(async_client, admin, "of_a", ["archives:read_all", "printers:read"], printer_ids=[prints["a"]])
  107. [planter] = [r for r in await _rows(async_client, headers=_auth(jwt)) if r["Print Name"] == "Planter"]
  108. assert (planter["Print ID"], planter["Status"], planter["Printer ID"], planter["Tags"]) == ("", "", "", "garden")
  109. async def test_a_print_running_now_is_a_row_of_its_own(async_client, db_session, prints):
  110. """Not taken from the archive, whose fields may be another printer's."""
  111. from backend.app.models.print_log import RUN_OPEN
  112. hook = await db_session.get(PrintArchive, prints["legacy"])
  113. hook.printer_id = prints["b"]
  114. running = PrintLogEntry(archive_id=prints["legacy"], printer_id=prints["a"], status=RUN_OPEN, started_at=T2)
  115. db_session.add(running)
  116. await db_session.commit()
  117. running_id = running.id
  118. [row] = [r for r in await _rows(async_client) if r["Print Name"] == "Hook"]
  119. assert (row["Print ID"], row["Status"], row["Printer ID"]) == (str(running_id), "printing", str(prints["a"]))
  120. async def test_only_the_exports_own_columns(async_client, prints):
  121. response = await async_client.get("/api/v1/archives/export", params={"fields": "id,confirm_token"})
  122. assert response.status_code == 400
  123. assert "confirm_token" in response.text
  124. async def test_a_print_logged_before_it_kept_its_quantity_has_its_archives(async_client, db_session, prints):
  125. archive = await db_session.get(PrintArchive, prints["planter"])
  126. archive.quantity = 4
  127. await db_session.commit()
  128. planter = [r for r in await _rows(async_client) if r["Print Name"] == "Planter"]
  129. assert {r["Items Printed"] for r in planter} == {"4"}
  130. async def test_a_print_logged_before_runs_names_its_printer(async_client, prints):
  131. [hook] = [r for r in await _rows(async_client) if r["Print Name"] == "Hook"]
  132. assert hook["Printer"] == "A"
  133. async def test_several_statuses_at_once(async_client, prints):
  134. rows = await _rows(async_client, status="failed,completed")
  135. assert sorted(r["Print ID"] for r in rows) == sorted(["", str(prints["first"]), str(prints["second"])])