refactor(prezentacja): wspólne partiale formularza — koniec dublowania chart/compile
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m32s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 12s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 8s
build / build (push) Successful in 20s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m28s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 11s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s

Spłata długu: bloki opcji i tabele wyniku były kopiowane między „Horoskop" (/) i
„Skompiluj" (/compile). Duplikat już raz spowodował regresję (podsumowanie gubiło
opcje), a przy każdej nowej opcji rósł (dodawałem je 3× w dwóch miejscach).

- `_form_options.html` (NOWY): bloki opcji — stacje/tabele, porównanie domów,
  ustawienia aspektów. Dołączany przez oba formularze.
- `_result_tables.html`: chart.html PRZECHODZI na ten wspólny plik (compile już go
  używał od #46). Rysunki (koło/aspektarian/deklinacja/antyscja) zgrupowane razem,
  potem wspólne tabele. Dzięki temu widok główny i podsumowanie NIE MOGĄ się już
  rozjechać — jedno źródło prawdy.

Zero zmian zachowania: oba szablony renderują te same pola i tabele co wcześniej
(sprawdzone renderem na bogatym wyniku). Testy strukturalne przełączone na
odczyt wspólnych plików. Prezentacja 221.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #50.
This commit is contained in:
2026-08-03 18:09:54 +02:00
parent b36b3bee19
commit a9f2a038fa
6 changed files with 50 additions and 273 deletions
@@ -0,0 +1,21 @@
{# Opcje obliczeń — WSPÓLNE dla „Horoskop" (/) i „Skompiluj" (/compile). Jeden plik,
bo dodawaliśmy je już 3× w dwóch formularzach i groziło rozjechaniem (regresja
podsumowania). Wszystkie synchronizowane między zakładkami przez formsync.js. #}
<div class="opts">
<label><input type="checkbox" name="stations" value="true" {{ 'checked' if form.stations else '' }}>
licz stacje planet (wolniejsze)</label>
<label><input type="checkbox" name="tables" value="true" {{ 'checked' if form.tables else '' }}>
tabele dodatkowe: żywioły, faza Księżyca, godziny planetarne (wolniejsze)</label>
</div>
<div class="opts" title="Domy dzielą to samo niebo inaczej. Zaznacz kilka, by porównać kuspy obok siebie (PRE-05). Kosmogram rysuje system wybrany wyżej.">
{% set chosen = form.house_systems or [] %}
<span class="muted small">Porównaj systemy domów:</span>
<label><input type="checkbox" name="house_systems" value="whole_sign" {{ 'checked' if 'whole_sign' in chosen else '' }}> Whole Sign</label>
<label><input type="checkbox" name="house_systems" value="equal" {{ 'checked' if 'equal' in chosen else '' }}> Equal</label>
<label><input type="checkbox" name="house_systems" value="porphyry" {{ 'checked' if 'porphyry' in chosen else '' }}> Porphyry</label>
</div>
<div class="opts" title="Orb = dopuszczalne odchylenie od dokładnego kąta aspektu. „Bonus świateł" powiększa orb dla aspektów ze Słońcem/Księżycem. Aspekty poboczne: półsekstyl 30°, półkwadratura 45°, kwinkunks 150° (PRE-06).">
<label>Orb aspektów (°) <input type="number" name="aspect_orb" step="0.5" min="1" max="15" value="{{ form.aspect_orb if form.aspect_orb is not none else 8 }}"></label>
<label>Bonus świateł (°) <input type="number" name="aspect_luminary_bonus" step="0.5" min="0" max="5" value="{{ form.aspect_luminary_bonus if form.aspect_luminary_bonus is not none else 2 }}"></label>
<label><input type="checkbox" name="aspect_minor" value="true" {{ 'checked' if form.aspect_minor else '' }}> aspekty poboczne (30/45/150°)</label>
</div>
+2 -243
View File
@@ -51,24 +51,7 @@
</label>
</div>
{% include "_location_picker.html" %}
<div class="opts">
<label><input type="checkbox" name="stations" value="true" {{ 'checked' if form.stations else '' }}>
licz stacje planet (wolniejsze)</label>
<label><input type="checkbox" name="tables" value="true" {{ 'checked' if form.tables else '' }}>
tabele dodatkowe: żywioły, faza Księżyca, godziny planetarne (wolniejsze)</label>
</div>
<div class="opts" title="Domy dzielą to samo niebo inaczej. Zaznacz kilka, by porównać kuspy obok siebie (PRE-05). Kosmogram rysuje system wybrany wyżej.">
{% set chosen = form.house_systems or [] %}
<span class="muted small">Porównaj systemy domów:</span>
<label><input type="checkbox" name="house_systems" value="whole_sign" {{ 'checked' if 'whole_sign' in chosen else '' }}> Whole Sign</label>
<label><input type="checkbox" name="house_systems" value="equal" {{ 'checked' if 'equal' in chosen else '' }}> Equal</label>
<label><input type="checkbox" name="house_systems" value="porphyry" {{ 'checked' if 'porphyry' in chosen else '' }}> Porphyry</label>
</div>
<div class="opts" title="Orb = dopuszczalne odchylenie od dokładnego kąta aspektu. „Bonus świateł" powiększa orb dla aspektów ze Słońcem/Księżycem. Aspekty poboczne: półsekstyl 30°, półkwadratura 45°, kwinkunks 150° (PRE-06).">
<label>Orb aspektów (°) <input type="number" name="aspect_orb" step="0.5" min="1" max="15" value="{{ form.aspect_orb if form.aspect_orb is not none else 8 }}"></label>
<label>Bonus świateł (°) <input type="number" name="aspect_luminary_bonus" step="0.5" min="0" max="5" value="{{ form.aspect_luminary_bonus if form.aspect_luminary_bonus is not none else 2 }}"></label>
<label><input type="checkbox" name="aspect_minor" value="true" {{ 'checked' if form.aspect_minor else '' }}> aspekty poboczne (30/45/150°)</label>
</div>
{% include "_form_options.html" %}
{% if location_label %}<p class="muted small">Wstępnie wpisano lokalizację: <strong>{{ location_label }}</strong> ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".</p>{% endif %}
<div class="actions">
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
@@ -104,102 +87,6 @@
</figure>
{% endif %}
{% if result.angles %}
<table class="angles">
<thead><tr><th></th><th>Znak</th><th>W znaku</th></tr></thead>
<tbody>
{% for key in ["Asc", "MC", "Dsc", "IC"] %}
{% set a = result.angles[key] %}
<tr><td>{{ a.name }}</td><td>{{ a.sign }}</td><td class="mono">{{ a.in_sign }}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# Porównanie systemów domów (PRE-05) — te same osie, inny podział na domy #}
{% if result.house_systems and result.house_systems | length > 1 %}
{% set HSN = {'whole_sign': 'Whole Sign', 'equal': 'Equal', 'porphyry': 'Porphyry'} %}
<div class="meta" title="Osie (Asc/MC) są wspólne — różni się podział na domy. Kosmogram rysuje pierwszy z listy.">Porównanie systemów domów ({{ result.house_systems | length }})</div>
<table class="angles">
<thead><tr><th>Dom</th>{% for hs in result.house_systems %}<th>{{ HSN.get(hs.system, hs.system) }}</th>{% endfor %}</tr></thead>
<tbody>
{% for i in range(12) %}
<tr><td>{{ i + 1 }}</td>
{% for hs in result.house_systems %}
<td class="mono"><span class="glyph">{{ hs.cusps[i].sign_glyph }}</span> {{ hs.cusps[i].in_sign }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<table>
<thead>
<tr>
<th title="Symbol astrologiczny (LOG-22)">Sym.</th>
<th>Obiekt</th><th>Znak</th><th>W znaku</th><th>Dom</th><th>Kier.</th><th>Prędkość °/d</th>
<th title="Deklinacja — odległość od równika niebieskiego. OOB = poza zakresem Słońca">Dekl.</th>
</tr>
</thead>
<tbody>
{% for p in result.positions %}
<tr>
<td class="glyph">{{ p.glyph or '' }}{% if p.sign_glyph %} {{ p.sign_glyph }}{% endif %}</td>
<td>{{ p.name }}</td>
<td>{{ p.sign }}</td>
<td class="mono">{{ p.in_sign }}</td>
<td>{{ p.house if p.house is defined else '—' }}</td>
<td class="{{ 'retro' if p.direction == 'Rx' else '' }}">{{ p.direction }}</td>
<td class="mono">{{ '%+.4f' | format(p.speed) }}</td>
<td class="mono">{% if p.declination is defined %}{{ '%+.2f°' | format(p.declination) }}{% if p.out_of_bounds %} <span class="badge" title="Out of bounds — deklinacja poza zakresem Słońca">OOB</span>{% endif %}{% else %}—{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if result.lots %}
<div class="meta">Lots hermetyczne ({{ result.lots | length }}) · sekta: <strong>{{ result.sect }}</strong></div>
<table class="angles">
<thead><tr><th>Lot</th><th>Znak</th><th>W znaku</th><th>Dom</th><th>Formuła</th></tr></thead>
<tbody>
{% for l in result.lots %}
<tr><td>{{ l.name }}</td><td>{{ l.sign }}</td><td class="mono">{{ l.in_sign }}</td><td>{{ l.house }}</td><td class="muted small">{{ l.formula }}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% set with_stations = result.positions | selectattr('stations', 'defined') | list %}
{% if with_stations %}
<div class="meta">Stacje planet (poprzednia / następna; <span class="badge">blisko</span> = mniej niż 7 dni)</div>
<table class="angles">
<thead><tr><th>Planeta</th><th>Poprzednia</th><th>Następna</th></tr></thead>
<tbody>
{% for p in with_stations %}
<tr>
<td>{{ p.name }}{% if p.stations.station_soon %} <span class="badge">blisko</span>{% endif %}</td>
<td class="mono">{% if p.stations.prev %}{{ p.stations.prev.type }} · {{ p.stations.prev.date }} · {{ p.stations.prev.degree }} ({{ p.stations.prev.days }} d){% else %}—{% endif %}</td>
<td class="mono">{% if p.stations.next %}{{ p.stations.next.type }} · {{ p.stations.next.date }} · {{ p.stations.next.degree }} (+{{ p.stations.next.days }} d){% else %}—{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if result.aspects %}
<div class="meta">Aspekty główne ({{ result.aspects | length }})</div>
<table class="angles">
<thead><tr><th>Obiekt 1</th><th>Aspekt</th><th>Obiekt 2</th><th>Orb</th><th title="A = aplikacyjny (dokładność nastąpi), S = separacyjny (już minęła)">A/S</th></tr></thead>
<tbody>
{% for a in result.aspects %}
<tr><td>{{ a.obj1 }}</td><td>{% if a.glyph %}<span class="glyph">{{ a.glyph }}</span> {% endif %}{{ a.aspect }}</td><td>{{ a.obj2 }}</td><td class="mono">{{ '%.2f'|format(a.orb) }}°</td><td>{{ a['as'] if a['as'] is defined else '—' }}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# Aspekty pozazodiakalne (LOG-07): paralele deklinacji i antyscja #}
{% if declination_svg %}
<figure class="declination-fig">
{{ declination_svg | safe }}
@@ -207,24 +94,6 @@
</figure>
{% endif %}
{% if result.parallels %}
<div class="meta" title="Ciała na tej samej (paralela) lub przeciwnej (kontrparalela) deklinacji — działają jak koniunkcja / opozycja poza ekliptyką">Paralele deklinacji ({{ result.parallels | length }})</div>
<table class="angles">
<thead><tr><th>Obiekt 1</th><th>Rodzaj</th><th>Obiekt 2</th><th>Orb</th><th>Dekl.</th></tr></thead>
<tbody>
{% for r in result.parallels %}
<tr>
<td>{{ r.obj1 }}</td>
<td>{{ 'paralela' if r.type == 'parallel' else 'kontrparalela' }}</td>
<td>{{ r.obj2 }}</td>
<td class="mono">{{ '%.2f'|format(r.orb) }}°</td>
<td class="mono">{{ '%+.2f'|format(r.dec1) }} / {{ '%+.2f'|format(r.dec2) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if antiscia_svg %}
<figure class="antiscia-fig">
{{ antiscia_svg | safe }}
@@ -232,117 +101,7 @@
</figure>
{% endif %}
{% if result.antiscia %}
<div class="meta" title="Odbicie względem osi przesileń (antyscja) lub równonocy (kontrantyscja) — punkty „dzielące” tę samą długość dnia">Antyscja ({{ result.antiscia | length }})</div>
<table class="angles">
<thead><tr><th>Obiekt 1</th><th>Rodzaj</th><th>Obiekt 2</th><th>Orb</th></tr></thead>
<tbody>
{% for r in result.antiscia %}
<tr>
<td>{{ r.obj1 }}</td>
<td>{{ 'antyscja' if r.type == 'antiscion' else 'kontrantyscja' }}</td>
<td>{{ r.obj2 }}</td>
<td class="mono">{{ '%.2f'|format(r.orb) }}°</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if result.tables %}
{% set tb = result.tables %}
<div class="meta">Tabele dodatkowe (LOG-23)</div>
{% set bal = tb.tally.with_modern_10_plus_asc %}
<table class="angles">
<thead><tr><th>Bilans (10 planet + Asc)</th><th colspan="4">Rozkład</th></tr></thead>
<tbody>
<tr><td>Żywioły</td>
{% for e, n in bal.elements.items() %}
<td>{{ tb.tally.labels.elements[e] }}: <strong>{{ n }}</strong></td>
{% endfor %}
</tr>
<tr><td>Jakości</td>
{% for q, n in bal.qualities.items() %}
<td>{{ tb.tally.labels.qualities[q] }}: <strong>{{ n }}</strong></td>
{% endfor %}
<td></td>
</tr>
</tbody>
</table>
{% if tb.tally.missing_elements or tb.tally.missing_qualities %}
<p class="muted small">Brak:
{% for e in tb.tally.missing_elements %}<strong>{{ tb.tally.labels.elements[e] }}</strong>{{ ", " if not loop.last }}{% endfor %}
{% for q in tb.tally.missing_qualities %}<strong>{{ tb.tally.labels.qualities[q] }}</strong>{{ ", " if not loop.last }}{% endfor %}
</p>
{% endif %}
{% if tb.moon_phase %}
<p class="muted small">
<strong>Faza Księżyca:</strong> {{ tb.moon_phase.phase_pl }} ·
elongacja {{ '%.2f'|format(tb.moon_phase.angle) }}° ·
oświetlenie {{ '%.1f'|format(tb.moon_phase.illumination * 100) }}% ·
{{ 'przybywa' if tb.moon_phase.waxing else 'ubywa' }}
</p>
{% endif %}
{% if tb.planetary_hours %}
{% set ph = tb.planetary_hours %}
<p class="muted small">
<strong>Godziny planetarne:</strong> władca dnia {{ ph.day_ruler }} ·
{{ ph.hour_number }}. godzina ({{ ph.period }}), władca {{ ph.hour_ruler }} ·
godzina trwa {{ ph.hour_length_minutes }} min
</p>
{% endif %}
{% if tb.prenatal_syzygy %}
{% set s = tb.prenatal_syzygy %}
<p class="muted small">
<strong>Syzygia prenatalna:</strong> {{ s.type_pl }} {{ s.when_utc }}
({{ s.days_before_birth }} dni przed) w {{ s.in_sign }}
</p>
{% endif %}
{% if tb.critical_degrees %}
<div class="meta">Stopnie krytyczne</div>
<table class="angles">
<thead><tr><th>Obiekt</th><th>Pozycja</th><th>Uwaga</th></tr></thead>
<tbody>
{% for c in tb.critical_degrees %}
<tr><td>{{ c.name }}</td><td class="mono">{{ c.in_sign }}</td>
<td class="muted small">{{ c.flags | join('; ') }}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<details class="loc">
<summary>Podziały: dwunastniki (D12) i nawamsa (D9)</summary>
<table>
<thead><tr><th>Obiekt</th><th>D12</th><th>D9</th></tr></thead>
<tbody>
{% for d in tb.divisional %}
<tr><td>{{ d.name }}</td><td class="mono">{{ d.d12_in_sign }}</td>
<td class="mono">{{ d.d9_in_sign }}</td></tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{% if result.cusps %}
<details class="loc">
<summary>Cusps domów ({{ result.house_system }})</summary>
<table>
<thead><tr><th>Dom</th><th>Znak</th><th>Cusp</th></tr></thead>
<tbody>
{% for c in result.cusps %}
<tr><td>{{ c.house }}</td><td>{{ c.sign }}</td><td class="mono">{{ c.in_sign }}</td></tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{% include "_result_tables.html" %}
{% endif %}
<script src="{{ static('now.js') }}"></script>
@@ -55,26 +55,9 @@ zapamiętane predykcje okresowe. Dane pobiera z pozostałych zakładek — nie t
</select>
</label>
</div>
{# Te same opcje co na „Horoskop" — synchronizowane między zakładkami (formsync),
żeby podsumowanie liczyło się z tym, co wybrano przy horoskopie (nie okrojone). #}
<div class="opts">
<label><input type="checkbox" name="stations" value="true" {{ 'checked' if form.stations else '' }}>
licz stacje planet (wolniejsze)</label>
<label><input type="checkbox" name="tables" value="true" {{ 'checked' if form.tables else '' }}>
tabele dodatkowe: żywioły, faza Księżyca, godziny planetarne (wolniejsze)</label>
</div>
<div class="opts">
{% set chosen = form.house_systems or [] %}
<span class="muted small">Porównaj systemy domów:</span>
<label><input type="checkbox" name="house_systems" value="whole_sign" {{ 'checked' if 'whole_sign' in chosen else '' }}> Whole Sign</label>
<label><input type="checkbox" name="house_systems" value="equal" {{ 'checked' if 'equal' in chosen else '' }}> Equal</label>
<label><input type="checkbox" name="house_systems" value="porphyry" {{ 'checked' if 'porphyry' in chosen else '' }}> Porphyry</label>
</div>
<div class="opts" title="Te same ustawienia aspektów co przy horoskopie (PRE-06). Aspekty poboczne: półsekstyl 30°, półkwadratura 45°, kwinkunks 150°.">
<label>Orb aspektów (°) <input type="number" name="aspect_orb" step="0.5" min="1" max="15" value="{{ form.aspect_orb if form.aspect_orb is not none else 8 }}"></label>
<label>Bonus świateł (°) <input type="number" name="aspect_luminary_bonus" step="0.5" min="0" max="5" value="{{ form.aspect_luminary_bonus if form.aspect_luminary_bonus is not none else 2 }}"></label>
<label><input type="checkbox" name="aspect_minor" value="true" {{ 'checked' if form.aspect_minor else '' }}> aspekty poboczne (30/45/150°)</label>
</div>
{# Te same opcje co na „Horoskop" (wspólny plik) — synchronizowane między
zakładkami (formsync), żeby podsumowanie liczyło się z tym, co wybrano. #}
{% include "_form_options.html" %}
<div class="actions">
<button type="submit">Złóż podsumowanie</button>
<button type="button" id="pdfBtn" class="ghost"
@@ -10,6 +10,7 @@ import pathlib
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
CHART = (APP / "templates" / "chart.html").read_text(encoding="utf-8")
COMPILE = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
OPTS = (APP / "templates" / "_form_options.html").read_text(encoding="utf-8") # wspólne opcje obu formularzy
MAIN = (APP / "main.py").read_text(encoding="utf-8")
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
SYNC = (APP / "static" / "formsync.js").read_text(encoding="utf-8")
@@ -19,9 +20,11 @@ FIELDS = ("aspect_orb", "aspect_luminary_bonus", "aspect_minor")
def test_both_forms_have_aspect_settings():
"""Pola są we WSPÓLNYM pliku opcji, który oba formularze dołączają."""
for f in FIELDS:
assert f'name="{f}"' in CHART, f"Horoskop: brak {f}"
assert f'name="{f}"' in COMPILE, f"Skompiluj: brak {f}"
assert f'name="{f}"' in OPTS, f"brak {f} we wspólnym pliku opcji"
assert '{% include "_form_options.html" %}' in CHART
assert '{% include "_form_options.html" %}' in COMPILE
def test_handlers_accept_and_pass_aspect_settings():
+4 -2
View File
@@ -167,9 +167,11 @@ def test_does_nothing_on_other_tabs():
# porównania domów, więc podsumowanie miało braki względem tego, co policzono.
def test_compile_form_carries_the_options():
"""Te same opcje co „Horoskop" (synchronizowane) — inaczej nie da się ich przekazać."""
"""Te same opcje co „Horoskop" — z WSPÓLNEGO pliku (formsync je synchronizuje)."""
assert '{% include "_form_options.html" %}' in TPL
opts = (APP / "templates" / "_form_options.html").read_text(encoding="utf-8")
for f in ('name="stations"', 'name="tables"', 'name="house_systems"'):
assert f in TPL, f"brak opcji w formularzu podsumowania: {f}"
assert f in opts, f"brak opcji we wspólnym pliku: {f}"
def test_compile_handler_recomputes_with_all_options():
@@ -7,14 +7,23 @@ import pathlib
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
CHART = (APP / "templates" / "chart.html").read_text(encoding="utf-8")
COMPILE = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
OPTS = (APP / "templates" / "_form_options.html").read_text(encoding="utf-8") # wspólne opcje
TABLES = (APP / "templates" / "_result_tables.html").read_text(encoding="utf-8") # wspólne tabele
MAIN = (APP / "main.py").read_text(encoding="utf-8")
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
def test_both_forms_use_shared_option_partial():
"""Opcje w jednym pliku — oba formularze go dołączają (koniec duplikacji)."""
assert '{% include "_form_options.html" %}' in CHART
assert '{% include "_form_options.html" %}' in COMPILE
def test_form_lets_you_pick_extra_systems():
"""Checkboxy dla trzech systemów — wielokrotny wybór (name powtórzony)."""
for val in ("whole_sign", "equal", "porphyry"):
assert f'name="house_systems" value="{val}"' in CHART, f"brak checkboxa {val}"
assert f'name="house_systems" value="{val}"' in OPTS, f"brak checkboxa {val}"
def test_handler_accepts_and_passes_house_systems():
@@ -28,16 +37,16 @@ def test_client_forwards_house_systems_to_logic():
def test_comparison_table_renders_only_for_multiple_systems():
"""Bez dodatkowych systemów nie pokazujemy pustej tabeli porównania."""
assert "result.house_systems | length > 1" in CHART
assert "Porównanie systemów domów" in CHART
assert "result.house_systems | length > 1" in TABLES
assert "Porównanie systemów domów" in TABLES
def test_comparison_table_shows_cusp_per_system():
"""Tabela iteruje po systemach i po 12 domach — kusp w każdej komórce."""
assert "for hs in result.house_systems" in CHART
assert "hs.cusps[i].in_sign" in CHART
assert "for hs in result.house_systems" in TABLES
assert "hs.cusps[i].in_sign" in TABLES
def test_checkbox_state_survives_submit():
"""Zaznaczone systemy zostają zaznaczone po przeliczeniu."""
assert "in chosen" in CHART and "form.house_systems" in CHART
assert "in chosen" in OPTS and "form.house_systems" in OPTS