diff --git a/changelog.txt b/changelog.txt index ea6380c..39dd0c8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,4 @@ +- Bugfix: Bei kleinen Verfahren oder großem Maßstab überlappten sich Legenden, Quellenangabe und Kartenschild, weil sie zusammen höher als die Karte waren. Beim Anlegen einer §38-/§41-Vorlage werden die Legenden jetzt bei Bedarf zwei- oder dreispaltig gesetzt (Kartenrand wird dafür breiter); reicht auch das nicht, wird die Seite höher und Quellenangabe und Kartenschild rutschen nach unten. Größe und Lage der Karte bleiben gleich (#93) - Bugfix: Weiße Basemap-Flächen ohne Kontur (z. B. „Platz“) waren in der Basemap-Legende auf weißem Grund unsichtbar; ihr Legendensymbol bekommt jetzt einen dünnen grauen Rand (0,15 mm), die Karte bleibt unverändert (#92) - Bugfix: Punktlinien des Basemap-Layers (Baumreihe, Hecke, Durchlass, Seilbahnen u. a., 12 Styles) wurden in Karte und Legende nicht gezeichnet – basemap.de nutzt Strichlänge 0 mit runder Kappe, Qt zeichnet Striche der Länge 0 nicht. Neuer Knopf „Punktlinien korrigieren“ im Basemap-Bereich des Druck-Tabs ersetzt die Strichlänge 0 durch 0,01 (festes Muster und zoomabhängiger Ausdruck); die Korrektur lässt sich als Stilvorlage sichern (#90) - Basemap-Legende für Drucklayouts (#88): Beim Erstellen einer §38-/§41-Vorlage wird ausgewertet, welche Objekte des basemap.de-Vektorkachel-Layers im Verfahrensgebiet (wahlweise im Druckbereich) tatsächlich dargestellt werden; ein Auswahldialog lässt Kategorien abwählen, die gewählten erscheinen mit der Signatur des Projektlayers in einem eigenen Legendenelement „Kartengrundlage (basemap.de)“ über der Quellenangabe. Zuordnung von 434 Basemap-Styles zu 190 Legendeneinträgen (Beschriftungen, Nummernschilder und Hintergrund ausgenommen) in `data/basemap_legende.json` (erzeugt mit `modules/basemap_katalog.py`, fachlich nachbearbeitet) diff --git a/functions/qgiscore_wrapper.py b/functions/qgiscore_wrapper.py index c336054..ded0ef2 100644 --- a/functions/qgiscore_wrapper.py +++ b/functions/qgiscore_wrapper.py @@ -894,6 +894,7 @@ QgsSymbolLayerUtils: Type[Any] QgsLayoutMeasurement: Type[Any] QgsLayoutUtils: Type[Any] QgsSimpleLineSymbolLayer: Type[Any] +QgsLayoutExporter: Type[Any] SELECT_BEHAVIOR_SET: Any = None SELECT_RELATION_INTERSECT: Any = None PROPERTY_TYPE_EXPRESSION: Any = None @@ -923,6 +924,7 @@ try: QgsLayoutUtils as _QgsLayoutUtils, QgsSymbolLayer as _QgsSymbolLayer, QgsSimpleLineSymbolLayer as _QgsSimpleLineSymbolLayer, + QgsLayoutExporter as _QgsLayoutExporter, Qgis as _QgisVT, ) @@ -943,6 +945,7 @@ try: QgsLayoutMeasurement = _QgsLayoutMeasurement QgsLayoutUtils = _QgsLayoutUtils QgsSimpleLineSymbolLayer = _QgsSimpleLineSymbolLayer + QgsLayoutExporter = _QgsLayoutExporter SELECT_BEHAVIOR_SET = _QgisVT.SelectBehavior.SetSelection SELECT_RELATION_INTERSECT = _QgisVT.SelectGeometryRelationship.Intersect @@ -1128,6 +1131,15 @@ except Exception: QgsSimpleLineSymbolLayer = _MockQgsSimpleLineSymbolLayer + class _MockQgsLayoutExporter: + def __init__(self, layout: Any = None): + self._layout = layout + + def renderRegionToImage(self, region: Any, size: Any = None, dpi: float = -1) -> Any: + return None + + QgsLayoutExporter = _MockQgsLayoutExporter + # QgsRectangle hatte im allgemeinen Mock-Block keinen Ersatz (nur Typannotation). if "QgsRectangle" not in globals(): class _MockQgsRectangle: diff --git a/modules/print_kartenrand.py b/modules/print_kartenrand.py new file mode 100644 index 0000000..aa6dc3d --- /dev/null +++ b/modules/print_kartenrand.py @@ -0,0 +1,189 @@ +""" +sn_basis/modules/print_kartenrand.py – Kartenrand neben der Karte überlappungsfrei anordnen (Basis #93) + +Im Standardlayout (§ 38, § 41) stehen rechts neben der Karte Hauptlegende, Basemap-Legende, +Quellenangabe und Kartenschild. Bei kleinen Verfahren oder großem Maßstab ist die Karte +niedriger als diese Elemente zusammen, und sie überlappten sich. ``ordne_kartenrand_an`` +läuft nach dem Aufbau des Layouts: + +1. Die Legenden werden für 1, 2 und 3 Spalten vermessen. Die Hauptlegende filtert nach + Karteninhalt; ihre Höhe steht erst nach einmaligem Zeichnen fest, deshalb wird der + Legendenbereich vorher einmal gerendert. +2. Gewählt wird die kleinste Spaltenzahl, bei der alles neben die Karte passt; der Kartenrand + wird dafür so weit verbreitert, wie die Legenden es brauchen. +3. Reicht es auch dreispaltig nicht, wird die Seite höher; Quellenangabe und Kartenschild + rutschen nach unten. + +Größe und Lage der Karte bleiben unverändert. +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable + +from sn_basis.functions.qgiscore_wrapper import QgsLayoutExporter, QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes +from sn_basis.functions.qt_wrapper import QSize + +MM = QgsUnitTypes.LayoutMillimeters + +RAND_MM = 10.0 # Seitenrand und Abstand Karte → Kartenrand +MIN_SPALTENBREITE_MM = 180.0 # Breite von Kartenschild und Quellenangabe +ABSTAND_LEGENDEN_MM = 4.0 # unter jeder Legende +QUELLE_HOEHE_MM = 20.0 +ABSTAND_QUELLE_SCHILD_MM = 10.0 +SCHILD_HOEHE_MM = 92.0 +MAX_SPALTEN = 3 +VERMESSUNG_DPI = 25 # Auflösung des einmaligen Zeichnens zum Vermessen + +HAUPTKARTE_ID = "Hauptkarte" +LEGENDEN_IDS = ("Legende", "Legende Basemap") # von oben nach unten +BASEMAP_LEGENDE_ID = "Legende Basemap" +QUELLENANGABE_ID = "Quellenangabe" +# Elemente unterhalb der Legenden, die bei höherer Seite gemeinsam nach unten rutschen +UNTERE_ELEMENTE = ( + QUELLENANGABE_ID, "Kartenschild", + "Kartenschild_Linie_1", "Kartenschild_Linie_2", "Kartenschild_Linie_3", + "Kartenschild_Linie_4", "Kartenschild_Linie_5", "Kartenschild_Linie_6", + "Herausgeber", "Bezeichnung_Flurbereinigung", "Name_Flurbereinigung", "Stadt_Gemeinde", "Landkreis", + "Kartenname", "Gefertigt", "Geprüft", "Massstab", "Ausgabedatum", "Aktenzeichen", +) + + +@dataclass +class Anordnung: + spalten: int + spaltenbreite: float + seite_breite: float + seite_hoehe: float + verschiebung_y: float # um so viel rutschen Quellenangabe und Kartenschild nach unten + + +def berechne_anordnung( + karte: tuple[float, float, float, float], + seite: tuple[float, float], + legenden_je_spalten: dict[int, list[tuple[float, float]]], +) -> Anordnung: + """Berechnet Spaltenzahl, Seitengröße und Verschiebung des Kartenschilds. + + Parameters + ---------- + karte : (links, oben, Breite, Höhe) der Karte in mm + seite : (Breite, Höhe) der Seite in mm + legenden_je_spalten : je Spaltenzahl 1–3 die Größen (Breite, Höhe) aller Legenden, von oben nach unten + """ + links, oben, breite, hoehe = karte + spalte_links = links + breite + RAND_MM + fest = QUELLE_HOEHE_MM + ABSTAND_QUELLE_SCHILD_MM + SCHILD_HOEHE_MM + + def bedarf(spalten: int) -> tuple[float, float]: + groessen = legenden_je_spalten.get(spalten, []) + benoetigt = sum(h + ABSTAND_LEGENDEN_MM for _, h in groessen) + fest + spaltenbreite = max([MIN_SPALTENBREITE_MM] + [b for b, _ in groessen]) + return benoetigt, spaltenbreite + + # Kleinste Spaltenzahl, die neben die Karte passt; passt keine, die mit dem geringsten Platzbedarf + # (bei Gleichstand weniger Spalten – mehr Spalten ohne Gewinn verbreitern nur den Rand). + kandidaten = [(n, *bedarf(n)) for n in range(1, MAX_SPALTEN + 1)] + passend = [k for k in kandidaten if k[1] <= hoehe] + spalten, benoetigt, spaltenbreite = passend[0] if passend else min(kandidaten, key=lambda k: (k[1], k[0])) + + verschiebung = max(0.0, benoetigt - hoehe) + return Anordnung( + spalten=spalten, + spaltenbreite=spaltenbreite, + seite_breite=max(seite[0], spalte_links + spaltenbreite + RAND_MM), + seite_hoehe=max(seite[1], oben + benoetigt + RAND_MM) if verschiebung else seite[1], + verschiebung_y=verschiebung, + ) + + +# --------------------------------------------------------- +# Anwendung auf ein Layout +# --------------------------------------------------------- + +def _aufruf(objekt: Any, methode: str, *args: Any, standard: Any = None) -> Any: + funktion = getattr(objekt, methode, None) + if not callable(funktion): + return standard + try: + return funktion(*args) + except Exception: + return standard + + +def _wert(objekt: Any, name: str) -> float: + wert = getattr(objekt, name) + return float(wert() if callable(wert) else wert) + + +def _rechteck(element: Any) -> tuple[float, float, float, float]: + r = element.sceneBoundingRect() + return _wert(r, "x"), _wert(r, "y"), _wert(r, "width"), _wert(r, "height") + + +def _legende_groesse(legende: Any, spalten: int) -> tuple[float, float]: + _aufruf(legende, "setColumnCount", spalten) + _aufruf(legende, "setSplitLayer", True) + _aufruf(legende, "setEqualColumnWidth", True) + _aufruf(legende, "adjustBoxSize") + groesse = legende.sizeWithUnits() + return _wert(groesse, "width"), _wert(groesse, "height") + + +def _rendere_legenden(layout: Any, legenden: list[Any]) -> None: + """Zeichnet die Legenden einmal mit geringer Auflösung, damit Kartenfilter und Größe feststehen.""" + exporter = QgsLayoutExporter(layout) + for legende in legenden: + _aufruf(exporter, "renderRegionToImage", legende.sceneBoundingRect(), QSize(), VERMESSUNG_DPI) + + +def _verschiebe(element: Any, dy: float) -> None: + position = element.positionWithUnits() + element.attemptMove(QgsLayoutPoint(_wert(position, "x"), _wert(position, "y") + dy, MM)) + + +def ordne_kartenrand_an(layout: Any, rendern: Callable[[Any, list], None] | None = None) -> Anordnung | None: + """Ordnet den Kartenrand eines Standardlayouts überlappungsfrei an (Seite ggf. breiter/höher). + + Returns + ------- + Anordnung | None + Die angewandte Anordnung oder ``None``, wenn das Layout keine Hauptkarte hat. + """ + karte = _aufruf(layout, "itemById", HAUPTKARTE_ID) + if karte is None: + return None + legenden = [l for l in (_aufruf(layout, "itemById", i) for i in LEGENDEN_IDS) if l is not None] + for legende in legenden: + _aufruf(legende, "setLocked", False) + (rendern or _rendere_legenden)(layout, legenden) + + groessen = {n: [_legende_groesse(l, n) for l in legenden] for n in range(1, MAX_SPALTEN + 1)} + seite = layout.pageCollection().page(0) + seitengroesse = seite.pageSize() + anordnung = berechne_anordnung(_rechteck(karte), (_wert(seitengroesse, "width"), _wert(seitengroesse, "height")), + groessen) + + for legende in legenden: + _legende_groesse(legende, anordnung.spalten) + + if (anordnung.seite_breite, anordnung.seite_hoehe) != (_wert(seitengroesse, "width"), _wert(seitengroesse, "height")): + seite.setPageSize(QgsLayoutSize(anordnung.seite_breite, anordnung.seite_hoehe, MM)) + + if anordnung.verschiebung_y > 0: + quelle = _aufruf(layout, "itemById", QUELLENANGABE_ID) + quelle_oben = _rechteck(quelle)[1] if quelle is not None else None + for element_id in UNTERE_ELEMENTE: + element = _aufruf(layout, "itemById", element_id) + if element is not None: + _verschiebe(element, anordnung.verschiebung_y) + basemap = _aufruf(layout, "itemById", BASEMAP_LEGENDE_ID) + if basemap is not None and quelle_oben is not None: + # unten verankert (siehe basemap_legende_layout.platziere): direkt über die verschobene Quellenangabe + position = basemap.positionWithUnits() + basemap.attemptMove(QgsLayoutPoint(_wert(position, "x"), + quelle_oben + anordnung.verschiebung_y - ABSTAND_LEGENDEN_MM, MM)) + + for legende in legenden: + _aufruf(legende, "setLocked", True) + return anordnung diff --git a/modules/print_layout.py b/modules/print_layout.py index e7e23af..294cf0e 100644 --- a/modules/print_layout.py +++ b/modules/print_layout.py @@ -1086,11 +1086,27 @@ class PrintLayout: if basemap_legende: self._erzeuge_basemap_legende(layout) + self._ordne_kartenrand_an(layout) + layout_manager.addLayout(layout) self._current_layout = None open_layout_designer(layout) return layout + def _ordne_kartenrand_an(self, layout: Any) -> None: + """Ordnet Legenden, Quellenangabe und Kartenschild überlappungsfrei an (Basis #93). + + Rand ggf. breiter (bis zu drei Legendenspalten) und Seite ggf. höher; die Karte bleibt. + Ein Fehler dabei darf das Anlegen der Vorlage nicht verhindern; er wird als Warnung gemeldet. + """ + from sn_basis.functions.message_wrapper import warning + from sn_basis.modules.print_kartenrand import ordne_kartenrand_an + + try: + ordne_kartenrand_an(layout) + except Exception as fehler: + warning("Vorlage anlegen", f"Der Kartenrand konnte nicht angeordnet werden: {fehler}") + def _erzeuge_basemap_legende(self, layout: Any) -> None: """Kennzeichnet das Layout und erzeugt die Basemap-Legende (Basis #88). diff --git a/tests/test_print_kartenrand.py b/tests/test_print_kartenrand.py new file mode 100644 index 0000000..a2ca1db --- /dev/null +++ b/tests/test_print_kartenrand.py @@ -0,0 +1,185 @@ +""" +Tests für modules/print_kartenrand.py – Anordnung des Kartenrands neben der Karte (Basis #93). + +Bei kleinen Verfahren oder großem Maßstab ist die Karte niedriger als Legenden, Quellenangabe +und Kartenschild zusammen; die Elemente überlappten sich. Die Anordnung wählt bis zu drei +Legendenspalten (Kartenrand wird dafür breiter) und vergrößert notfalls die Seitenhöhe. Karte +bleibt in Größe und Lage gleich. Die Wirkung im echten Layout ist im QGIS-Test geprüft. +""" +import unittest +from unittest.mock import MagicMock, patch + +from sn_basis.modules import print_kartenrand as kr +from sn_basis.modules.print_layout import PrintLayout +from sn_basis.modules.print_kartenrand import berechne_anordnung, ordne_kartenrand_an + +KARTE = (10.0, 10.0, 360.0, 300.0) # links, oben, Breite, Höhe in mm +SEITE = (580.0, 320.0) +FEST = kr.QUELLE_HOEHE_MM + kr.ABSTAND_QUELLE_SCHILD_MM + kr.SCHILD_HOEHE_MM # 122 mm + + +def _legenden(*je_spalten): + """Eine Legende je Argument: (w1, h1, w2, h2, w3, h3).""" + return {n: [(l[2 * (n - 1)], l[2 * (n - 1) + 1]) for l in je_spalten] for n in (1, 2, 3)} + + +class TestBerechneAnordnung(unittest.TestCase): + + def test_passt_einspaltig_nichts_aendert_sich(self): + """Prüft, dass bei genug Platz neben der Karte einspaltig angeordnet wird und Seite und Kartenschild bleiben.""" + anordnung = berechne_anordnung(KARTE, SEITE, _legenden((80, 100, 120, 50, 160, 34))) + self.assertEqual(anordnung.spalten, 1) + self.assertEqual((anordnung.seite_breite, anordnung.seite_hoehe), SEITE) + self.assertEqual(anordnung.verschiebung_y, 0.0) + self.assertEqual(anordnung.spaltenbreite, kr.MIN_SPALTENBREITE_MM) + + def test_mehr_spalten_und_breiterer_rand(self): + """Prüft, dass bei zu hohen Legenden die kleinste passende Spaltenzahl gewählt und der Kartenrand dafür verbreitert wird.""" + # Hauptlegende + Basemap-Legende; einspaltig 250 mm, zweispaltig 140 mm hoch – passt in 300 mm Karte ab 2 Spalten + legenden = _legenden((150, 150, 300, 80, 450, 55), (170, 100, 340, 60, 510, 40)) + anordnung = berechne_anordnung(KARTE, SEITE, legenden) + self.assertEqual(anordnung.spalten, 2) + self.assertEqual(anordnung.spaltenbreite, 340) + spalte_links = KARTE[0] + KARTE[2] + kr.RAND_MM + self.assertEqual(anordnung.seite_breite, spalte_links + 340 + kr.RAND_MM) + self.assertEqual(anordnung.seite_hoehe, SEITE[1]) + self.assertEqual(anordnung.verschiebung_y, 0.0) + + def test_seite_wird_hoeher_wenn_auch_drei_spalten_nicht_reichen(self): + """Prüft, dass die Seite höher wird und Quellenangabe und Kartenschild nach unten rutschen, wenn auch drei Spalten nicht reichen.""" + karte = (10.0, 10.0, 160.0, 80.0) # kleines Verfahren + anordnung = berechne_anordnung(karte, (380.0, 100.0), _legenden((150, 300, 300, 160, 450, 110))) + self.assertEqual(anordnung.spalten, 3) + benoetigt = 110 + kr.ABSTAND_LEGENDEN_MM + FEST + self.assertAlmostEqual(anordnung.seite_hoehe, karte[1] + benoetigt + kr.RAND_MM) + self.assertAlmostEqual(anordnung.verschiebung_y, benoetigt - karte[3]) + self.assertEqual(anordnung.spaltenbreite, 450) + + def test_seite_wird_nie_kleiner(self): + """Prüft, dass Seitenbreite und -höhe nie unter die ursprüngliche Seitengröße fallen.""" + anordnung = berechne_anordnung(KARTE, (900.0, 600.0), _legenden((80, 50, 120, 30, 160, 20))) + self.assertEqual((anordnung.seite_breite, anordnung.seite_hoehe), (900.0, 600.0)) + + def test_ohne_legenden_nur_kartenschild(self): + """Prüft, dass ohne Legenden nur Quellenangabe und Kartenschild zählen und die Seite bei zu niedriger Karte höher wird.""" + karte = (10.0, 10.0, 100.0, 100.0) + anordnung = berechne_anordnung(karte, (310.0, 120.0), {1: [], 2: [], 3: []}) + self.assertEqual(anordnung.spalten, 1) + self.assertAlmostEqual(anordnung.verschiebung_y, FEST - 100.0) + self.assertAlmostEqual(anordnung.seite_hoehe, 10.0 + FEST + kr.RAND_MM) + + +# ---------------------------------------------------------------- Layout-Fakes + +class _Punkt: + def __init__(self, x, y): self._x, self._y = x, y + def x(self): return self._x + def y(self): return self._y + + +class _Element: + def __init__(self, x, y, b, h): + self.x, self.y, self.b, self.h = x, y, b, h + self.verschoben = [] + + def positionWithUnits(self): return _Punkt(self.x, self.y) + def sceneBoundingRect(self): + return type("R", (), {"x": lambda s: self.x, "y": lambda s: self.y, + "width": lambda s: self.b, "height": lambda s: self.h})() + def attemptMove(self, punkt): + neu_y = punkt.y() if callable(punkt.y) else punkt.y + self.verschoben.append(neu_y) + self.y = neu_y + + +class _Legende(_Element): + def __init__(self, x, y, groessen, unten_verankert=False): + super().__init__(x, y, groessen[1][0], groessen[1][1]) + self.groessen, self.spalten, self.unten = groessen, 1, unten_verankert + + def setColumnCount(self, n): self.spalten = n + def setSplitLayer(self, wert): pass + def setEqualColumnWidth(self, wert): pass + def setLocked(self, wert): pass + def adjustBoxSize(self): + self.b, self.h = self.groessen[self.spalten] + def sizeWithUnits(self): + return type("S", (), {"width": lambda s: self.b, "height": lambda s: self.h})() + def attemptResize(self, groesse): pass + + +class _Seite: + def __init__(self, b, h): self.b, self.h = b, h + def pageSize(self): return type("S", (), {"width": lambda s: self.b, "height": lambda s: self.h})() + def setPageSize(self, groesse): + self.b = groesse.width() if callable(groesse.width) else groesse.width + self.h = groesse.height() if callable(groesse.height) else groesse.height + + +class _Layout: + def __init__(self, elemente, seite): + self.elemente, self.seite = elemente, seite + + def itemById(self, element_id): return self.elemente.get(element_id) + def pageCollection(self): return type("P", (), {"page": lambda s, i: self.seite})() + + +class TestOrdneKartenrandAn(unittest.TestCase): + + def _layout(self, legende_groessen): + karte = _Element(10, 10, 160, 80) + elemente = { + "Hauptkarte": karte, + "Legende": _Legende(180, 10, legende_groessen), + "Quellenangabe": _Element(180, -32, 180, 20), + "Kartenschild": _Element(180, -2, 180, 92), + "Aktenzeichen": _Element(196, 87, 171, 4), + } + return _Layout(elemente, _Seite(380, 100)), elemente + + def test_seite_hoeher_und_kartenschild_verschoben(self): + """Prüft, dass bei zu niedriger Karte die Legende dreispaltig wird, die Seite wächst und Quellenangabe und Kartenschild um denselben Betrag nach unten rutschen.""" + layout, elemente = self._layout({1: (150, 300), 2: (300, 160), 3: (450, 110)}) + anordnung = ordne_kartenrand_an(layout, rendern=lambda *a: None) + self.assertEqual(elemente["Legende"].spalten, 3) + self.assertGreater(layout.seite.h, 100) + dy = anordnung.verschiebung_y + self.assertGreater(dy, 0) + self.assertAlmostEqual(elemente["Quellenangabe"].y, -32 + dy) + self.assertAlmostEqual(elemente["Kartenschild"].y, -2 + dy) + self.assertAlmostEqual(elemente["Aktenzeichen"].y, 87 + dy) + self.assertEqual(elemente["Hauptkarte"].verschoben, []) + self.assertEqual(layout.seite.b, 180 + 450 + kr.RAND_MM) + + def test_passt_nichts_wird_verschoben(self): + """Prüft, dass bei ausreichend hoher Karte weder Seite noch Kartenschild verändert werden.""" + layout, elemente = self._layout({1: (100, 10), 2: (200, 6), 3: (300, 4)}) + elemente["Hauptkarte"].h = 300 + anordnung = ordne_kartenrand_an(layout, rendern=lambda *a: None) + self.assertEqual(anordnung.spalten, 1) + self.assertEqual((layout.seite.b, layout.seite.h), (380, 100)) + self.assertEqual(elemente["Kartenschild"].verschoben, []) + + def test_ohne_karte_keine_anordnung(self): + """Prüft, dass ein Layout ohne Hauptkarte unverändert bleibt.""" + self.assertIsNone(ordne_kartenrand_an(_Layout({}, _Seite(100, 100)), rendern=lambda *a: None)) + + +class TestAnbindungPrintLayout(unittest.TestCase): + + def test_vorlage_ordnet_kartenrand_an(self): + """Prüft, dass PrintLayout beim Anlegen einer Vorlage den Kartenrand anordnen lässt.""" + layout = MagicMock() + with patch("sn_basis.modules.print_kartenrand.ordne_kartenrand_an") as anordnen: + PrintLayout(project=MagicMock())._ordne_kartenrand_an(layout) + anordnen.assert_called_once_with(layout) + + def test_fehler_bei_der_anordnung_verhindert_die_vorlage_nicht(self): + """Prüft, dass ein Fehler bei der Anordnung nur als Warnung gemeldet wird und keine Ausnahme durchreicht.""" + with patch("sn_basis.modules.print_kartenrand.ordne_kartenrand_an", side_effect=RuntimeError("kaputt")), patch("sn_basis.functions.message_wrapper.warning") as warnung: + PrintLayout(project=MagicMock())._ordne_kartenrand_an(MagicMock()) + self.assertIn("kaputt", warnung.call_args[0][1]) + + +if __name__ == "__main__": + unittest.main()