Skip to content

Commit 489d743

Browse files
itsab1989claude
andcommitted
Measurement Report: save PDF next to the original file, not the temp folder + tidy header (Knut)
- Imported i1Profiler measurements convert into a temp folder; the PDF and Reveal defaulted there ('/private/var/folders/.../T/chromiq_report_…'). Each source now remembers its ORIGINAL path, and the PDF/Reveal anchor on that folder. - The per-page report header listed every profile name; with many imported measurements that's unwieldy, so past a handful it collapses to the run count + date range. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 61bbe3e commit 489d743

2 files changed

Lines changed: 60 additions & 15 deletions

File tree

tests/test_i1profiler_convert_report_fixes.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,30 @@ def fake_convert(src, argyll, out_dir):
9898
assert seen["src"] == txt
9999

100100

101+
def test_report_pdf_anchors_on_origin_not_temp(qapp, tmp_path, monkeypatch):
102+
"""Imported measurements convert into a temp folder, but the PDF / Reveal must
103+
default to the user's ORIGINAL folder, never the temp one (Knut)."""
104+
from ui.dialogs.measurement_report_dialog import MeasurementReportDialog as M
105+
106+
monkeypatch.setattr(
107+
"workflow.measurement_report.list_project_reports", lambda d: [])
108+
origin = tmp_path / "myfolder" / "Epson_2026-01-06.txt"
109+
origin.parent.mkdir(parents=True)
110+
origin.write_text("x")
111+
temp_ti3 = tmp_path / "chromiq_report_xyz" / "Epson_2026-01-06.ti3"
112+
temp_ti3.parent.mkdir()
113+
temp_ti3.write_text("y")
114+
115+
host = types.SimpleNamespace(_sources=[], _ti3=None)
116+
host._source_key = types.MethodType(M._source_key, host)
117+
host._gather_runs = lambda t: (t.stem, [{"created": "2026-01-01"}])
118+
types.MethodType(M._append_source, host)(temp_ti3, origin=origin)
119+
120+
anchor = types.MethodType(M._anchor_dir, host)()
121+
assert anchor == origin.parent # user's folder…
122+
assert anchor != temp_ti3.parent # …not the temp conversion folder
123+
124+
101125
def test_report_as_ti3_raises_on_bad_file(qapp, settings, tmp_path, monkeypatch):
102126
"""_as_ti3 raises on a bad file so the batch adder can list what failed."""
103127
from ui.dialogs.measurement_report_dialog import MeasurementReportDialog

ui/dialogs/measurement_report_dialog.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -689,27 +689,32 @@ def _source_key(self, ti3: Path) -> tuple:
689689
return ("dir", str(ti3.parent))
690690
return ("file", str(ti3))
691691

692-
def _append_source(self, ti3: Path) -> bool:
692+
def _append_source(self, ti3: Path, origin: "Path | None" = None) -> bool:
693693
"""Add one measurement to the source list (no repaint). Returns False if it
694694
is already present or has no runs. Raises on a gather error, so a batch add
695-
can report which files failed."""
695+
can report which files failed.
696+
697+
*origin* is the file the user actually picked (the same as *ti3* for a
698+
ChromIQ .ti3, but the original .mxf/.txt/.cxf when *ti3* is a temp
699+
conversion). The report is saved next to the origin, never the temp folder
700+
(Knut)."""
696701
key = self._source_key(ti3)
697702
if any(s.get("key") == key for s in self._sources):
698703
return False
699704
name, runs = self._gather_runs(ti3)
700705
if not runs:
701706
return False
702707
self._sources.append({"key": key, "name": name, "dir": ti3.parent,
703-
"ti3": ti3, "runs": runs})
708+
"ti3": ti3, "origin": Path(origin or ti3), "runs": runs})
704709
if self._ti3 is None:
705710
self._ti3 = ti3
706711
return True
707712

708-
def _add_source(self, ti3: Path) -> None:
713+
def _add_source(self, ti3: Path, origin: "Path | None" = None) -> None:
709714
"""Add a single measurement and repaint (used when opening the report on
710715
one file)."""
711716
try:
712-
added = self._append_source(ti3)
717+
added = self._append_source(ti3, origin)
713718
except Exception as exc: # noqa: BLE001
714719
self._view.setHtml(self._error_html(str(exc)))
715720
return
@@ -755,7 +760,7 @@ def _on_add_project(self) -> None:
755760
added, failed = 0, []
756761
for path in paths:
757762
try:
758-
if self._append_source(self._as_ti3(Path(path))):
763+
if self._append_source(self._as_ti3(Path(path)), origin=Path(path)):
759764
added += 1
760765
except Exception as exc: # noqa: BLE001
761766
failed.append(f"{Path(path).name}{exc}")
@@ -848,21 +853,31 @@ def _trend_configs(self) -> list:
848853
corner_metrics, None, 1, False),
849854
]
850855

856+
def _anchor_dir(self) -> Path:
857+
"""The folder the PDF and Reveal default to: the folder of the FIRST
858+
source's ORIGINAL file — a ChromIQ run folder, or the user's own folder for
859+
an imported measurement — never the temp folder an i1Profiler file is
860+
converted into (Knut)."""
861+
if self._sources:
862+
return self._sources[0]["origin"].parent
863+
return self._ti3.parent if self._ti3 else Path.cwd()
864+
851865
def _profile_root(self) -> Path:
852866
"""The profile's project folder (``<project>/runs/<id>`` → ``<project>``),
853-
or the run folder itself for a browsed external ``.ti3`` that isn't in a
867+
or the folder itself for a browsed/imported measurement that isn't in a
854868
ChromIQ project layout."""
855-
run_dir = self._ti3.parent if self._ti3 else Path.cwd()
869+
run_dir = self._anchor_dir()
856870
if run_dir.parent.name == "runs":
857871
return run_dir.parents[1]
858872
return run_dir
859873

860874
def _report_dir(self) -> Path:
861875
"""Where a PDF is saved (Knut): an all-runs report belongs to the whole
862876
profile, so it goes in a ``reports`` folder next to ``runs/``; a single-run
863-
report goes in that run's own ``reports`` folder."""
877+
report goes in that run's own ``reports`` folder. For an imported
878+
measurement it goes in a ``reports`` folder next to the file itself."""
864879
from core.file_manager import reports_subdir
865-
run_dir = self._ti3.parent
880+
run_dir = self._anchor_dir()
866881
if self._all_runs_check.isChecked() and run_dir.parent.name == "runs":
867882
return reports_subdir(self._profile_root())
868883
return reports_subdir(run_dir)
@@ -872,7 +887,7 @@ def _on_reveal(self) -> None:
872887
the reports folder and open saved PDFs (Knut)."""
873888
from PyQt6.QtCore import QUrl
874889
from PyQt6.QtGui import QDesktopServices
875-
if self._ti3:
890+
if self._sources or self._ti3:
876891
QDesktopServices.openUrl(QUrl.fromLocalFile(str(self._profile_root())))
877892

878893
def _export_pdf(self) -> None:
@@ -1045,12 +1060,18 @@ def _scope_header_units(self, runs: list) -> list:
10451060
from workflow.measurement_report import report_scope
10461061
sc = report_scope(runs)
10471062
names = list(dict.fromkeys(p["name"] for p in sc["profiles"])) # de-dup, ordered
1048-
units = [n + ("," if i < len(names) - 1 else "")
1049-
for i, n in enumerate(names)]
10501063
d0, d1 = sc["date_range"]
10511064
n = sc["total"]
1052-
units.append(" " + tr("{n} measurement run").format(n=n) if n == 1
1053-
else " " + tr("{n} measurement runs").format(n=n))
1065+
# Listing every name gets unwieldy once there are many (e.g. a folder of
1066+
# imported measurements); past a handful, drop the names and let the run
1067+
# count + date range speak for the scope (Knut).
1068+
if len(names) <= 4:
1069+
units = [nm + ("," if i < len(names) - 1 else "")
1070+
for i, nm in enumerate(names)]
1071+
units.append(" " + (tr("{n} measurement run").format(n=n) if n == 1
1072+
else tr("{n} measurement runs").format(n=n)))
1073+
else:
1074+
units = [tr("{n} measurement runs").format(n=n)]
10541075
units[-1] += f" ({d0}{d1})"
10551076
# Trailing space on the name units so they don't run together.
10561077
return [(u + " ") if u.endswith(",") else u for u in units]

0 commit comments

Comments
 (0)