feat(filter): Berlin-Bezirk filter in Einstellungen

Adds a collapsible 12-Bezirk checkbox list to the filter tab. The UI
speaks Bezirke; internally the match runs on the PLZ extracted from
flat.address and resolved to a dominant Bezirk via a curated 187-PLZ
map (berlin_districts.py).

- Migration 0011 adds user_filters.districts (CSV of selected names)
- Empty stored value = no filter = all Bezirke ticked in the UI.
  Submitting "all ticked" or "none ticked" both normalise to empty
  so the defaults and the nuclear state mean the same thing.
- When a Bezirk filter is active, flats with an unknown/unmapped PLZ
  are excluded — if the user bothered to narrow by district, sneaking
  in unplaceable flats would be the wrong default.
- Filter summary on the Wohnungen page shows "N Bezirke" so it's
  visible the filter is active.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
EiSiMo 2026-04-23 10:05:55 +02:00
parent abd614604f
commit d13f9c5b6e
7 changed files with 202 additions and 20 deletions

View file

@ -126,7 +126,7 @@ def _is_htmx(request: Request) -> bool:
# ---------------------------------------------------------------------------
FILTER_KEYS = ("rooms_min", "rooms_max", "max_rent", "min_size",
"wbs_required", "max_age_hours")
"wbs_required", "max_age_hours", "districts")
def _has_filters(f) -> bool:
@ -175,6 +175,13 @@ def _filter_summary(f) -> str:
parts.append("ohne WBS")
if f["max_age_hours"]:
parts.append(f"{int(f['max_age_hours'])} h alt")
try:
districts_csv = (f["districts"] or "").strip()
except (KeyError, IndexError):
districts_csv = ""
if districts_csv:
n = sum(1 for d in districts_csv.split(",") if d.strip())
parts.append(f"{n} Bezirk{'e' if n != 1 else ''}")
return " · ".join(parts)