From f904a8ee40685e2b6f227b20c7724fe1e1733952 Mon Sep 17 00:00:00 2001 From: nagisml Date: Tue, 29 Sep 2026 15:36:47 +0200 Subject: [PATCH 1/5] new set_corrected_coords() which is used by the detail panel, the cache table's right-click menu and the map's right-click menu --- src/opensak/db/corrected_coords.py | 36 +++++++++++++ src/opensak/gui/cache_detail.py | 18 ++----- src/opensak/gui/cache_table.py | 19 ++----- src/opensak/gui/mainwindow.py | 20 ++----- tests/unit-tests/test_corrected_coords_db.py | 57 ++++++++++++++++++++ 5 files changed, 103 insertions(+), 47 deletions(-) create mode 100644 src/opensak/db/corrected_coords.py create mode 100644 tests/unit-tests/test_corrected_coords_db.py diff --git a/src/opensak/db/corrected_coords.py b/src/opensak/db/corrected_coords.py new file mode 100644 index 00000000..029d3fbe --- /dev/null +++ b/src/opensak/db/corrected_coords.py @@ -0,0 +1,36 @@ +""" +src/opensak/db/corrected_coords.py — Skriv korrigerede koordinater for en cache. + +Fælles DB-skrivning for alle steder der gemmer korrigerede koordinater +med det samme (detaljepanel, cache-tabellens højreklik-menu, kortets +højreklik-menu), så UserNote-rækken altid opdateres ens. +""" + +from __future__ import annotations +from typing import Optional + +from opensak.db.database import get_session +from opensak.db.models import Cache, UserNote +from opensak.utils.types import GcCode + + +def set_corrected_coords( + gc_code: GcCode, lat: Optional[float], lon: Optional[float] +) -> bool: + """Sæt (eller ryd, med lat/lon = None) korrigerede koordinater for gc_code. + + Opretter UserNote-rækken hvis den mangler. is_corrected sættes kun når + både lat og lon er angivet. Returnerer False hvis cachen ikke findes. + """ + with get_session() as session: + cache_row = session.query(Cache).filter_by(gc_code=gc_code).first() + if cache_row is None: + return False + note = cache_row.user_note + if note is None: + note = UserNote(cache_id=cache_row.id) + session.add(note) + note.corrected_lat = lat + note.corrected_lon = lon + note.is_corrected = lat is not None and lon is not None + return True diff --git a/src/opensak/gui/cache_detail.py b/src/opensak/gui/cache_detail.py index 29f9204c..3f7a2caa 100644 --- a/src/opensak/gui/cache_detail.py +++ b/src/opensak/gui/cache_detail.py @@ -462,21 +462,9 @@ def _clear_corrected_coords(self) -> None: self._save_corrected_coords(None, None) def _save_corrected_coords(self, lat, lon) -> None: - from opensak.db.database import get_session - from opensak.db.models import Cache as CacheModel, UserNote - with get_session() as session: - cache_row = session.query(CacheModel).filter_by( - gc_code=self._current_gc_code - ).first() - if not cache_row: - return - note = cache_row.user_note - if note is None: - note = UserNote(cache_id=cache_row.id) - session.add(note) - note.corrected_lat = lat - note.corrected_lon = lon - note.is_corrected = (lat is not None and lon is not None) + from opensak.db.corrected_coords import set_corrected_coords + if not set_corrected_coords(self._current_gc_code, lat, lon): + return self._corrected_lat = lat self._corrected_lon = lon diff --git a/src/opensak/gui/cache_table.py b/src/opensak/gui/cache_table.py index 3b3bc110..ad36c8a0 100644 --- a/src/opensak/gui/cache_table.py +++ b/src/opensak/gui/cache_table.py @@ -1849,22 +1849,11 @@ def _clear_corrected(self, cache: Cache) -> None: def _save_corrected(self, cache: Cache, lat, lon) -> None: from opensak.db.database import get_session - from opensak.db.models import UserNote, Cache as CacheModel + from opensak.db.models import Cache as CacheModel + from opensak.db.corrected_coords import set_corrected_coords from sqlalchemy.orm import joinedload, selectinload - with get_session() as session: - cache_row = session.query(CacheModel).options( - joinedload(CacheModel.user_note) - ).filter_by(gc_code=cache.gc_code).first() - if not cache_row: - return - note = cache_row.user_note - if note is None: - note = UserNote(cache_id=cache_row.id) - session.add(note) - session.flush() - note.corrected_lat = lat - note.corrected_lon = lon - note.is_corrected = (lat is not None and lon is not None) + if not set_corrected_coords(cache.gc_code, lat, lon): + return # Reload det fulde cache-objekt fra DB med user_note eager-loaded, # og erstat det detachede objekt i modellen direkte. diff --git a/src/opensak/gui/mainwindow.py b/src/opensak/gui/mainwindow.py index fbd784af..3b0bc272 100644 --- a/src/opensak/gui/mainwindow.py +++ b/src/opensak/gui/mainwindow.py @@ -1627,26 +1627,12 @@ def _on_map_cache_selected(self, gc_code: GcCode) -> None: def _on_set_corrected_from_map(self, gc_code: GcCode, lat: float, lon: float) -> None: """Sæt korrigerede koordinater på en cache via højreklik på kortet.""" - from opensak.db.database import get_session - from opensak.db.models import UserNote + from opensak.db.corrected_coords import set_corrected_coords from opensak.coords import format_coords from opensak.gui.settings import get_settings - from sqlalchemy.orm import joinedload - with get_session() as session: - cache = session.query(Cache).options( - joinedload(Cache.user_note) - ).filter_by(gc_code=gc_code).first() - if not cache: - return - note = cache.user_note - if note is None: - note = UserNote(cache_id=cache.id) - session.add(note) - note.corrected_lat = lat - note.corrected_lon = lon - note.is_corrected = True - session.commit() + if not set_corrected_coords(gc_code, lat, lon): + return coords = format_coords(lat, lon, get_settings().coord_format) self._statusbar.showMessage( diff --git a/tests/unit-tests/test_corrected_coords_db.py b/tests/unit-tests/test_corrected_coords_db.py new file mode 100644 index 00000000..41302a94 --- /dev/null +++ b/tests/unit-tests/test_corrected_coords_db.py @@ -0,0 +1,57 @@ +# tests/unit-tests/test_corrected_coords_db.py — shared corrected-coords DB write. + +from opensak.db.corrected_coords import set_corrected_coords +from opensak.db.database import get_session +from opensak.db.models import Cache, UserNote + + +def _note(gc_code: str): + with get_session() as s: + cache = s.query(Cache).filter_by(gc_code=gc_code).one() + note = cache.user_note + if note is None: + return None + return note.corrected_lat, note.corrected_lon, note.is_corrected + + +class TestSetCorrectedCoords: + def test_creates_user_note_when_missing(self, db_session, make_cache): + db_session.add(make_cache("GCSET1")) + db_session.commit() + + assert set_corrected_coords("GCSET1", 56.0, 13.0) is True + assert _note("GCSET1") == (56.0, 13.0, True) + + def test_updates_existing_note_and_keeps_text(self, db_session, make_cache): + cache = make_cache("GCSET2") + db_session.add(cache) + db_session.flush() + db_session.add(UserNote(cache_id=cache.id, note="keep me")) + db_session.commit() + + set_corrected_coords("GCSET2", 57.5, 14.25) + + assert _note("GCSET2") == (57.5, 14.25, True) + with get_session() as s: + assert s.query(UserNote).one().note == "keep me" + + def test_clear_resets_is_corrected(self, db_session, make_cache): + db_session.add(make_cache("GCSET3")) + db_session.commit() + set_corrected_coords("GCSET3", 56.0, 13.0) + + assert set_corrected_coords("GCSET3", None, None) is True + assert _note("GCSET3") == (None, None, False) + + def test_partial_coords_are_not_corrected(self, db_session, make_cache): + db_session.add(make_cache("GCSET4")) + db_session.commit() + + set_corrected_coords("GCSET4", 56.0, None) + + assert _note("GCSET4") == (56.0, None, False) + + def test_missing_cache_returns_false(self, db_session): + assert set_corrected_coords("GCNOPE", 1.0, 2.0) is False + with get_session() as s: + assert s.query(UserNote).count() == 0 From 96e30155ce2317eb86920ecbca37388f87ed7b7f Mon Sep 17 00:00:00 2001 From: nagisml Date: Tue, 29 Sep 2026 17:17:27 +0200 Subject: [PATCH 2/5] New feature show distance and bearing --- src/opensak/gui/cache_detail.py | 2 ++ tests/unit-tests/test_cache_detail.py | 28 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/opensak/gui/cache_detail.py b/src/opensak/gui/cache_detail.py index 3f7a2caa..0d661c04 100644 --- a/src/opensak/gui/cache_detail.py +++ b/src/opensak/gui/cache_detail.py @@ -450,6 +450,8 @@ def _edit_corrected_coords(self) -> None: from opensak.gui.dialogs.corrected_coords_dialog import CorrectedCoordsDialog dlg = CorrectedCoordsDialog( gc_code=self._current_gc_code, + orig_lat=self._current_lat, + orig_lon=self._current_lon, corrected_lat=self._corrected_lat, corrected_lon=self._corrected_lon, parent=self, diff --git a/tests/unit-tests/test_cache_detail.py b/tests/unit-tests/test_cache_detail.py index b2c7db5b..ba66a775 100644 --- a/tests/unit-tests/test_cache_detail.py +++ b/tests/unit-tests/test_cache_detail.py @@ -877,3 +877,31 @@ def test_real_html_description_still_renders_as_html(self, qapp, tmp_path, monke ) assert "Real" in panel._desc_view.toPlainText() assert "HTML" in panel._desc_view.toPlainText() + + +# ── Corrected coordinates dialog ────────────────────────────────────────────── + +def test_edit_corrected_passes_original_coords(monkeypatch, tmp_path, qapp): + # The detail panel must pass the cache's original coordinates so the + # dialog shows the same "Original coordinates" panel as the table and + # Edit Cache entry points. + monkeypatch.setattr(cd, "get_settings", lambda: _fake_settings()) + captured = {} + + class FakeDlg: + def __init__(self, **kwargs): + captured.update(kwargs) + + def exec(self): + return False + + monkeypatch.setattr( + "opensak.gui.dialogs.corrected_coords_dialog.CorrectedCoordsDialog", FakeDlg + ) + panel = CacheDetailPanel() + panel.show_cache(_load_cache(tmp_path, db_suffix="corr")) + panel._edit_corrected_coords() + + assert captured["gc_code"] == "GCNOTES1" + assert captured["orig_lat"] == pytest.approx(55.0) + assert captured["orig_lon"] == pytest.approx(10.0) From 1357cb1e881e3a3425af244f491b53b81dfdd171 Mon Sep 17 00:00:00 2001 From: nagisml Date: Tue, 29 Sep 2026 17:40:13 +0200 Subject: [PATCH 3/5] Show distance --- src/opensak/gui/cache_table.py | 32 +-------- .../gui/dialogs/corrected_coords_dialog.py | 41 +++++++++++- src/opensak/gui/mainwindow.py | 6 +- src/opensak/lang/cs.py | 1 + src/opensak/lang/da.py | 1 + src/opensak/lang/de.py | 1 + src/opensak/lang/de_CH.py | 1 + src/opensak/lang/en.py | 1 + src/opensak/lang/es.py | 1 + src/opensak/lang/fr.py | 1 + src/opensak/lang/nl.py | 1 + src/opensak/lang/pl.py | 1 + src/opensak/lang/pt.py | 1 + src/opensak/lang/se.py | 1 + tests/e2e-tests/test_e2e_corrected_coords.py | 62 +++++++++++++++++ tests/unit-tests/test_cache_table.py | 3 + .../test_corrected_coords_dialog.py | 67 +++++++++++++++++++ 17 files changed, 190 insertions(+), 32 deletions(-) diff --git a/src/opensak/gui/cache_table.py b/src/opensak/gui/cache_table.py index ad36c8a0..b365db7a 100644 --- a/src/opensak/gui/cache_table.py +++ b/src/opensak/gui/cache_table.py @@ -1848,38 +1848,12 @@ def _clear_corrected(self, cache: Cache) -> None: self._save_corrected(cache, None, None) def _save_corrected(self, cache: Cache, lat, lon) -> None: - from opensak.db.database import get_session - from opensak.db.models import Cache as CacheModel from opensak.db.corrected_coords import set_corrected_coords - from sqlalchemy.orm import joinedload, selectinload if not set_corrected_coords(cache.gc_code, lat, lon): return - - # Reload det fulde cache-objekt fra DB med user_note eager-loaded, - # og erstat det detachede objekt i modellen direkte. - # Det undgår alle problemer med at skrive til detached ORM-relationer. - with get_session() as session: - fresh = session.query(CacheModel).options( - joinedload(CacheModel.user_note), - selectinload(CacheModel.waypoints), - selectinload(CacheModel.attributes), - selectinload(CacheModel.trackables), - ).filter_by(gc_code=cache.gc_code).first() - if fresh is None: - return - - # Erstat objektet i listen — find det via gc_code - caches = self._model._caches - for i, c in enumerate(caches): - if c.gc_code == cache.gc_code: - caches[i] = fresh - break - - self._reset_model_preserving_selection() - # Issue #474: map wasn't refreshed when corrected coords were set/cleared - # via this (context menu) path — only the "Add corrected coordinates..." - # button in the cache detail panel emitted a change signal. Emit here too - # so mainwindow can update the map pin the same way for both entry points. + # Issue #474: mainwindow refreshes the table row (refresh_cache_row), + # map pin and detail panel from this signal — the same handler for + # every entry point, so no row reload here. self.corrected_coords_changed.emit(cache.gc_code) def _open_converter(self, lat: float, lon: float) -> None: diff --git a/src/opensak/gui/dialogs/corrected_coords_dialog.py b/src/opensak/gui/dialogs/corrected_coords_dialog.py index def1164c..f3c8f243 100644 --- a/src/opensak/gui/dialogs/corrected_coords_dialog.py +++ b/src/opensak/gui/dialogs/corrected_coords_dialog.py @@ -20,6 +20,10 @@ from opensak.lang import tr from opensak.coords import format_coords, parse_coords +from opensak.filters.engine import ( + DIRECTIONS, _bearing_deg, bearing_direction, distance_km, +) +from opensak.gui.dialogs.distance_bearing_dialog import _format_distance from opensak.gui.settings import get_settings from opensak.gui.theme import hint_style from opensak.utils.types import GcCode, CoordFormat @@ -164,6 +168,13 @@ def _setup_ui( self._corrected_panel_widget: Optional[QFrame] = None layout.addLayout(self._corrected_panel_container) + # ── Afstand og retning fra originale koordinater ────────────────────── + self._offset_lbl = QLabel("") + self._offset_lbl.setStyleSheet("font-weight: bold;") + self._offset_lbl.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + self._offset_lbl.setVisible(False) + layout.addWidget(self._offset_lbl) + # ── Knapper ─────────────────────────────────────────────────────────── btn_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | @@ -221,10 +232,36 @@ def _set_corrected_panel(self, lat: Optional[float], lon: Optional[float]) -> No self._corrected_lbl.setVisible(True) self._corrected_panel_widget = self._make_coords_panel(lat, lon) self._corrected_panel_container.addWidget(self._corrected_panel_widget) - self.adjustSize() else: self._corrected_lbl.setVisible(False) - self.adjustSize() + self._update_offset(lat, lon) + self.adjustSize() + + def _update_offset(self, lat: Optional[float], lon: Optional[float]) -> None: + """Vis afstand og retning fra originale til korrigerede koordinater. + + Skjules når originale koordinater mangler eller input er ugyldigt. + Fanger tastefejl (forkert minut-ciffer, forkert halvkugle) — mystery + final-koordinater ligger typisk inden for 2 mi / 3.2 km af de originale. + """ + if (lat is None or lon is None + or self._orig_lat is None or self._orig_lon is None): + self._offset_lbl.setVisible(False) + return + metres = distance_km(self._orig_lat, self._orig_lon, lat, lon) * 1000.0 + if metres < 0.5: + bearing = "—" + else: + deg = _bearing_deg(self._orig_lat, self._orig_lon, lat, lon) + dirs = dict(zip(DIRECTIONS, tr("bearing_dirs").split())) + code = bearing_direction(deg) + bearing = f"{deg:.0f}° {dirs.get(code, code)}" + self._offset_lbl.setText(tr( + "corrected_dialog_offset", + distance=_format_distance(metres, get_settings().use_miles), + bearing=bearing, + )) + self._offset_lbl.setVisible(True) def _on_accept(self) -> None: if self._lat is not None and self._lon is not None: diff --git a/src/opensak/gui/mainwindow.py b/src/opensak/gui/mainwindow.py index 3b0bc272..9e9f4ea7 100644 --- a/src/opensak/gui/mainwindow.py +++ b/src/opensak/gui/mainwindow.py @@ -1641,11 +1641,15 @@ def _on_set_corrected_from_map(self, gc_code: GcCode, lat: float, lon: float) -> self._on_corrected_coords_changed(gc_code) def _on_corrected_coords_changed(self, gc_code: GcCode) -> None: - """Update the map pin and table row after corrected coordinates change.""" + """Update the map pin, table row and detail panel (if this cache is + currently shown) after corrected coordinates change — whichever entry + point (detail panel, table context menu, map context menu) made it.""" self._cache_table.refresh_cache_row(gc_code) full = self._load_full_cache(gc_code) if full: self._map_widget.update_cache(full) + if getattr(self._detail_panel, "_current_gc_code", None) == gc_code: + self._detail_panel.show_cache(full) def _on_found_status_changed(self, gc_code: GcCode) -> None: """Issue #649: refresh table row, map pin, detail panel (if this diff --git a/src/opensak/lang/cs.py b/src/opensak/lang/cs.py index 293ae777..043a9abb 100644 --- a/src/opensak/lang/cs.py +++ b/src/opensak/lang/cs.py @@ -1002,6 +1002,7 @@ "corrected_dialog_original": "Původní souřadnice:", "corrected_dialog_corrected": "Opravené souřadnice:", "corrected_dialog_copy_tooltip": "Kopírovat do schránky", + "corrected_dialog_offset": "Vzdálenost od originálu: {distance} · Azimut: {bearing}", # ── Distance & bearing dialog ───────────────────────────────────────────── "dist_title": "Vzdálenost & Azimut", diff --git a/src/opensak/lang/da.py b/src/opensak/lang/da.py index 8f46d787..e2e1707d 100644 --- a/src/opensak/lang/da.py +++ b/src/opensak/lang/da.py @@ -1001,6 +1001,7 @@ "corrected_dialog_original": "Originale koordinater:", "corrected_dialog_corrected": "Korrigerede koordinater:", "corrected_dialog_copy_tooltip": "Kopiér til udklipsholder", + "corrected_dialog_offset": "Afstand fra original: {distance} · Retning: {bearing}", # ── Afstand & retning dialog ────────────────────────────────────────────── "dist_title": "Afstand & Retning", diff --git a/src/opensak/lang/de.py b/src/opensak/lang/de.py index 275fca6c..56ebecde 100644 --- a/src/opensak/lang/de.py +++ b/src/opensak/lang/de.py @@ -1002,6 +1002,7 @@ "corrected_dialog_original": "Originalkoordinaten:", "corrected_dialog_corrected": "Korrigierte Koordinaten:", "corrected_dialog_copy_tooltip": "In die Zwischenablage kopieren", + "corrected_dialog_offset": "Entfernung vom Original: {distance} · Richtung: {bearing}", # ── Distance & bearing dialog ───────────────────────────────────────────── "dist_title": "Entfernung & Peilung", diff --git a/src/opensak/lang/de_CH.py b/src/opensak/lang/de_CH.py index 206bd854..596191f7 100644 --- a/src/opensak/lang/de_CH.py +++ b/src/opensak/lang/de_CH.py @@ -1003,6 +1003,7 @@ "corrected_dialog_original": "Originalkoordinaten:", "corrected_dialog_corrected": "Korrigierte Koordinaten:", "corrected_dialog_copy_tooltip": "In die Zwischenablage kopieren", + "corrected_dialog_offset": "Entfernung vom Original: {distance} · Richtung: {bearing}", # ── Distance & bearing dialog ───────────────────────────────────────────── "dist_title": "Entfernung & Peilung", diff --git a/src/opensak/lang/en.py b/src/opensak/lang/en.py index 0c49d687..71a6a8a9 100644 --- a/src/opensak/lang/en.py +++ b/src/opensak/lang/en.py @@ -1000,6 +1000,7 @@ "corrected_dialog_original": "Original coordinates:", "corrected_dialog_corrected": "Corrected coordinates:", "corrected_dialog_copy_tooltip": "Copy to clipboard", + "corrected_dialog_offset": "Distance from original: {distance} · Bearing: {bearing}", # ── Distance & bearing dialog ───────────────────────────────────────────── "dist_title": "Distance & Bearing", diff --git a/src/opensak/lang/es.py b/src/opensak/lang/es.py index c4064cef..29352f13 100644 --- a/src/opensak/lang/es.py +++ b/src/opensak/lang/es.py @@ -1002,6 +1002,7 @@ "corrected_dialog_original": "Coordenadas originales:", "corrected_dialog_corrected": "Coordenadas corregidas:", "corrected_dialog_copy_tooltip": "Copiar al portapapeles", + "corrected_dialog_offset": "Distancia desde el original: {distance} · Rumbo: {bearing}", # ── Distance & bearing dialog ───────────────────────────────────────────── "dist_title": "Distancia y rumbo", diff --git a/src/opensak/lang/fr.py b/src/opensak/lang/fr.py index 1a0f3da3..642bb3db 100644 --- a/src/opensak/lang/fr.py +++ b/src/opensak/lang/fr.py @@ -1002,6 +1002,7 @@ "corrected_dialog_original": "Coordonnées d'origine :", "corrected_dialog_corrected": "Coordonnées corrigées :", "corrected_dialog_copy_tooltip": "Copier dans le presse-papiers", + "corrected_dialog_offset": "Distance depuis l'original : {distance} · Cap : {bearing}", # ── Distance & cap ──────────────────────────────────────────────────────── "dist_title": "Distance & Cap", diff --git a/src/opensak/lang/nl.py b/src/opensak/lang/nl.py index de7d3ae2..fda308d0 100644 --- a/src/opensak/lang/nl.py +++ b/src/opensak/lang/nl.py @@ -1003,6 +1003,7 @@ "corrected_dialog_original": "Originele coördinaten:", "corrected_dialog_corrected": "Gecorrigeerde coördinaten:", "corrected_dialog_copy_tooltip": "Kopiëren naar klembord", + "corrected_dialog_offset": "Afstand tot origineel: {distance} · Richting: {bearing}", # ── Distance & bearing dialog ───────────────────────────────────────────── "dist_title": "Afstand & Richting", diff --git a/src/opensak/lang/pl.py b/src/opensak/lang/pl.py index 1d09834b..a727ad9f 100644 --- a/src/opensak/lang/pl.py +++ b/src/opensak/lang/pl.py @@ -1002,6 +1002,7 @@ "corrected_dialog_original": "Oryginalne współrzędne:", "corrected_dialog_corrected": "Poprawione współrzędne:", "corrected_dialog_copy_tooltip": "Kopiuj do schowka", + "corrected_dialog_offset": "Odległość od oryginału: {distance} · Azymut: {bearing}", # ── Distance & bearing dialog ───────────────────────────────────────────── "dist_title": "Odległość i azymut", diff --git a/src/opensak/lang/pt.py b/src/opensak/lang/pt.py index 8c274916..23413e9b 100644 --- a/src/opensak/lang/pt.py +++ b/src/opensak/lang/pt.py @@ -1002,6 +1002,7 @@ "corrected_dialog_original": "Coordenadas originais:", "corrected_dialog_corrected": "Coordenadas corrigidas:", "corrected_dialog_copy_tooltip": "Copiar para a área de transferência", + "corrected_dialog_offset": "Distância do original: {distance} · Rumo: {bearing}", # ── Distance & bearing dialog ───────────────────────────────────────────── "dist_title": "Distância e Azimute", diff --git a/src/opensak/lang/se.py b/src/opensak/lang/se.py index 6111e1c6..ea2ab51a 100644 --- a/src/opensak/lang/se.py +++ b/src/opensak/lang/se.py @@ -1001,6 +1001,7 @@ "corrected_dialog_original": "Ursprungliga koordinater:", "corrected_dialog_corrected": "Korrigerade koordinater:", "corrected_dialog_copy_tooltip": "Kopiera till urklipp", + "corrected_dialog_offset": "Avstånd från original: {distance} · Riktning: {bearing}", # ── Distance & bearing dialog ───────────────────────────────────────────── "dist_title": "Avstånd & Bäring", diff --git a/tests/e2e-tests/test_e2e_corrected_coords.py b/tests/e2e-tests/test_e2e_corrected_coords.py index 40f9e229..8d59c26b 100644 --- a/tests/e2e-tests/test_e2e_corrected_coords.py +++ b/tests/e2e-tests/test_e2e_corrected_coords.py @@ -183,3 +183,65 @@ def test_clear_corrected_coords_removes_from_db(seeded_window, qtbot, monkeypatc if note is not None: assert note.corrected_lat is None assert note.corrected_lon is None + + +# ── End-to-end: detail panel follows changes from other entry points ───────── + + +def _table_cache(window, gc_code: str): + model = window._cache_table.model() + for row in range(model.rowCount()): + cache = model.cache_at(row) + if cache and cache.gc_code == gc_code: + return cache + pytest.fail(f"{gc_code} not found in table") + + +def test_table_context_menu_refreshes_detail_panel(seeded_window, qtbot): + # Setting/clearing corrected coords via the table context menu must update + # the detail panel when it shows that cache — not only table and map. + window = seeded_window + _select_cache(window, qtbot, "GC12345") + panel = window._detail_panel + assert panel._corrected_lat is None + + window._cache_table._save_corrected(_table_cache(window, "GC12345"), 56.0, 13.0) + qtbot.wait(100) + + assert panel._current_gc_code == "GC12345" + assert panel._corrected_lat == pytest.approx(56.0) + assert panel._corrected_lon == pytest.approx(13.0) + assert panel._corrected_frame.isVisibleTo(panel) + + window._cache_table._clear_corrected(_table_cache(window, "GC12345")) + qtbot.wait(100) + + assert panel._corrected_lat is None + assert not panel._corrected_frame.isVisibleTo(panel) + + +def test_map_context_menu_refreshes_detail_panel(seeded_window, qtbot): + # "Set corrected here" on the map must update the detail panel too. + window = seeded_window + _select_cache(window, qtbot, "GC12345") + panel = window._detail_panel + + window._on_set_corrected_from_map("GC12345", 56.5, 13.5) + qtbot.wait(100) + + assert panel._current_gc_code == "GC12345" + assert panel._corrected_lat == pytest.approx(56.5) + assert panel._corrected_lon == pytest.approx(13.5) + + +def test_change_on_other_cache_leaves_detail_panel_alone(seeded_window, qtbot): + # A change to a cache that isn't shown must not switch the detail panel. + window = seeded_window + _select_cache(window, qtbot, "GC12345") + panel = window._detail_panel + + window._on_set_corrected_from_map("GC99999", 56.5, 13.5) + qtbot.wait(100) + + assert panel._current_gc_code == "GC12345" + assert panel._corrected_lat is None diff --git a/tests/unit-tests/test_cache_table.py b/tests/unit-tests/test_cache_table.py index 141b5e23..50aa35f5 100644 --- a/tests/unit-tests/test_cache_table.py +++ b/tests/unit-tests/test_cache_table.py @@ -1493,6 +1493,8 @@ def test_save_and_clear_corrected(self, view, db_session, make_cache): db_session.commit() cache = _cache(gc_code="GCCC", latitude=55.0, longitude=12.0) view.load_caches([cache]) + # mainwindow refreshes the row from this signal (see _on_corrected_coords_changed) + view.corrected_coords_changed.connect(view.refresh_cache_row) view._save_corrected(cache, 56.0, 13.0) refreshed = view._model.cache_at(0) assert refreshed.user_note is not None @@ -1545,6 +1547,7 @@ def test_save_corrected_on_other_row_preserves_current_selection( cache_a = _cache(gc_code="GCA", latitude=55.0, longitude=12.0) cache_b = _cache(gc_code="GCB", latitude=56.0, longitude=13.0) view.load_caches([cache_a, cache_b]) + view.corrected_coords_changed.connect(view.refresh_cache_row) # Simulate the user having GCA selected (shown in the detail panel). # Use select_by_gc_code rather than assuming insertion order == row diff --git a/tests/unit-tests/test_corrected_coords_dialog.py b/tests/unit-tests/test_corrected_coords_dialog.py index f753c59f..7fc33e64 100644 --- a/tests/unit-tests/test_corrected_coords_dialog.py +++ b/tests/unit-tests/test_corrected_coords_dialog.py @@ -83,3 +83,70 @@ def test_copy_sets_clipboard(self, qtbot, settings): qtbot.addWidget(dlg) dlg._copy("hello world") assert QApplication.clipboard().text() == "hello world" + + +class TestOffsetFromOriginal: + # Distance & bearing from the original to the corrected coordinates. + + @pytest.fixture(autouse=True) + def english(self): + from opensak.lang import current_language, load_language + prev = current_language() + load_language("en") + yield + load_language(prev) + + def _dlg(self, qtbot, **kwargs): + dlg = CorrectedCoordsDialog("GC123", orig_lat=47.0, orig_lon=8.0, **kwargs) + qtbot.addWidget(dlg) + return dlg + + def test_shown_for_valid_input(self, qtbot, settings): + settings.use_miles = False + dlg = self._dlg(qtbot) + dlg._input.setText("47.01, 8.0") # ~1.112 km due north + assert dlg._offset_lbl.isVisibleTo(dlg) + text = dlg._offset_lbl.text() + assert "1.112 km" in text + assert "0° N" in text + + def test_bearing_east(self, qtbot, settings): + settings.use_miles = False + dlg = self._dlg(qtbot) + dlg._input.setText("47.0, 8.001") # ~76 m due east + assert "90° E" in dlg._offset_lbl.text() + assert " m" in dlg._offset_lbl.text() + + def test_miles_setting(self, qtbot, settings): + settings.use_miles = True + dlg = self._dlg(qtbot) + dlg._input.setText("47.01, 8.0") + assert "mi" in dlg._offset_lbl.text() + assert "km" not in dlg._offset_lbl.text() + + def test_same_point_has_no_bearing(self, qtbot, settings): + settings.use_miles = False + dlg = self._dlg(qtbot) + dlg._input.setText("47.0, 8.0") + assert "0.0 m" in dlg._offset_lbl.text() + assert "—" in dlg._offset_lbl.text() + + def test_prefilled_shows_offset(self, qtbot, settings): + settings.use_miles = False + dlg = self._dlg(qtbot, corrected_lat=47.01, corrected_lon=8.0) + assert dlg._offset_lbl.isVisibleTo(dlg) + + @pytest.mark.parametrize("text", ["", "not a coordinate"]) + def test_hidden_for_empty_or_invalid_input(self, qtbot, settings, text): + settings.use_miles = False + dlg = self._dlg(qtbot) + dlg._input.setText("47.01, 8.0") + dlg._input.setText(text) + assert not dlg._offset_lbl.isVisibleTo(dlg) + + def test_hidden_without_original_coords(self, qtbot, settings): + settings.use_miles = False + dlg = CorrectedCoordsDialog("GC123") + qtbot.addWidget(dlg) + dlg._input.setText("47.01, 8.0") + assert not dlg._offset_lbl.isVisibleTo(dlg) From 266da5a36e115f278e840d82ba6e1157153d6dbf Mon Sep 17 00:00:00 2001 From: nagisml Date: Tue, 29 Sep 2026 17:44:15 +0200 Subject: [PATCH 4/5] Allow removal of CC in CC dialog --- .../gui/dialogs/corrected_coords_dialog.py | 17 ++++++++++ tests/e2e-tests/test_e2e_corrected_coords.py | 33 +++++++++++++++++++ .../test_corrected_coords_dialog.py | 25 ++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/src/opensak/gui/dialogs/corrected_coords_dialog.py b/src/opensak/gui/dialogs/corrected_coords_dialog.py index f3c8f243..c0fa61be 100644 --- a/src/opensak/gui/dialogs/corrected_coords_dialog.py +++ b/src/opensak/gui/dialogs/corrected_coords_dialog.py @@ -184,6 +184,17 @@ def _setup_ui( btn_box.rejected.connect(self.reject) self._ok_btn = btn_box.button(QDialogButtonBox.StandardButton.Ok) self._ok_btn.setEnabled(False) + + # Fjern-knap — kun når cachen allerede har korrigerede koordinater. + # Accepterer dialogen med (None, None), som alle kaldere gemmer som "ryd". + self._remove_btn = btn_box.addButton( + tr("ctx_clear_corrected"), QDialogButtonBox.ButtonRole.ResetRole + ) + self._remove_btn.setToolTip(tr("detail_corrected_clear_tooltip")) + self._remove_btn.clicked.connect(self._on_remove) + self._remove_btn.setVisible( + current_lat is not None and current_lon is not None + ) layout.addWidget(btn_box) self._input.textChanged.connect(self._on_input_changed) @@ -267,6 +278,12 @@ def _on_accept(self) -> None: if self._lat is not None and self._lon is not None: self.accept() + def _on_remove(self) -> None: + """Ryd korrigerede koordinater: accepter med (None, None).""" + self._lat = None + self._lon = None + self.accept() + def get_coords(self) -> Tuple[Optional[float], Optional[float]]: """Returner de parsede koordinater.""" return self._lat, self._lon diff --git a/tests/e2e-tests/test_e2e_corrected_coords.py b/tests/e2e-tests/test_e2e_corrected_coords.py index 8d59c26b..84eea6b7 100644 --- a/tests/e2e-tests/test_e2e_corrected_coords.py +++ b/tests/e2e-tests/test_e2e_corrected_coords.py @@ -245,3 +245,36 @@ def test_change_on_other_cache_leaves_detail_panel_alone(seeded_window, qtbot): assert panel._current_gc_code == "GC12345" assert panel._corrected_lat is None + + +def test_remove_button_clears_corrected_coords(seeded_window, qtbot, monkeypatch): + # The dialog's "Clear corrected coordinates" button removes them from the + # DB and the detail panel. + from opensak.db.database import get_session + from opensak.db.models import Cache as CacheModel + from opensak.gui.dialogs.corrected_coords_dialog import CorrectedCoordsDialog + + window = seeded_window + _select_cache(window, qtbot, "GC12345") + panel = window._detail_panel + window._on_set_corrected_from_map("GC12345", 56.5, 13.5) + qtbot.wait(100) + assert panel._corrected_lat is not None + + class _Remove(CorrectedCoordsDialog): + def exec(self): + assert self._remove_btn.isVisibleTo(self) + self._remove_btn.click() + return self.result() + + monkeypatch.setattr( + "opensak.gui.dialogs.corrected_coords_dialog.CorrectedCoordsDialog", _Remove + ) + panel._edit_corrected_coords() + qtbot.wait(100) + + with get_session() as session: + note = session.query(CacheModel).filter_by(gc_code="GC12345").one().user_note + assert note.is_corrected is False + assert note.corrected_lat is None + assert panel._corrected_lat is None diff --git a/tests/unit-tests/test_corrected_coords_dialog.py b/tests/unit-tests/test_corrected_coords_dialog.py index 7fc33e64..6f926067 100644 --- a/tests/unit-tests/test_corrected_coords_dialog.py +++ b/tests/unit-tests/test_corrected_coords_dialog.py @@ -150,3 +150,28 @@ def test_hidden_without_original_coords(self, qtbot, settings): qtbot.addWidget(dlg) dlg._input.setText("47.01, 8.0") assert not dlg._offset_lbl.isVisibleTo(dlg) + + +class TestRemoveButton: + def test_hidden_without_existing_corrected(self, qtbot, settings): + dlg = CorrectedCoordsDialog("GC123", orig_lat=47.0, orig_lon=8.0) + qtbot.addWidget(dlg) + assert not dlg._remove_btn.isVisibleTo(dlg) + + def test_shown_with_existing_corrected(self, qtbot, settings): + dlg = CorrectedCoordsDialog( + "GC123", orig_lat=47.0, orig_lon=8.0, + corrected_lat=47.01, corrected_lon=8.0, + ) + qtbot.addWidget(dlg) + assert dlg._remove_btn.isVisibleTo(dlg) + + def test_remove_accepts_with_no_coords(self, qtbot, settings): + dlg = CorrectedCoordsDialog( + "GC123", corrected_lat=47.01, corrected_lon=8.0, + ) + qtbot.addWidget(dlg) + assert dlg.get_coords() != (None, None) + dlg._remove_btn.click() + assert dlg.result() == QDialog.DialogCode.Accepted + assert dlg.get_coords() == (None, None) From e8afcdc5a957dd02f789182dea189329ed6e0b71 Mon Sep 17 00:00:00 2001 From: nagisml Date: Tue, 29 Sep 2026 20:27:40 +0200 Subject: [PATCH 5/5] fix mypy --- src/opensak/gui/cache_detail.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/opensak/gui/cache_detail.py b/src/opensak/gui/cache_detail.py index 0d661c04..2fe82401 100644 --- a/src/opensak/gui/cache_detail.py +++ b/src/opensak/gui/cache_detail.py @@ -464,15 +464,17 @@ def _clear_corrected_coords(self) -> None: self._save_corrected_coords(None, None) def _save_corrected_coords(self, lat, lon) -> None: + gc_code = self._current_gc_code + if not gc_code: + return from opensak.db.corrected_coords import set_corrected_coords - if not set_corrected_coords(self._current_gc_code, lat, lon): + if not set_corrected_coords(gc_code, lat, lon): return self._corrected_lat = lat self._corrected_lon = lon self._update_corrected_ui() - if self._current_gc_code: - self.corrected_coords_changed.emit(self._current_gc_code) + self.corrected_coords_changed.emit(gc_code) def eventFilter(self, obj, event) -> bool: note_editor = getattr(self, "_note_editor", None)