""" Per-user filter matching. Each user has one row in user_filters. A flat matches the user if all of the user's non-null constraints are satisfied. Empty filters = matches all. """ import logging from typing import Iterable logger = logging.getLogger("web.matching") # German labels per filter dimension. The order also defines the order chips # appear in the "Rausgefilterte Wohnungen" section. _REASON_ORDER = ("Zimmer", "Preis", "Größe", "WBS", "Bezirk") def flat_filter_failures(flat: dict, f: dict | None) -> list[str]: """Return the German labels of filter dimensions the flat fails. Empty list = full match. Each label appears at most once (rooms_min and rooms_max both map to "Zimmer"), in stable _REASON_ORDER so chips render consistently. Berlin-Bezirk filter: empty string = no filter = all Bezirke match. When active, flats with an unknown/unmapped PLZ count as a Bezirk failure — if the user bothered to narrow by district, we shouldn't sneak in flats we couldn't place. """ if not f: return [] failures: set[str] = set() rooms = flat.get("rooms") or 0.0 rent = flat.get("total_rent") or 0.0 size = flat.get("size") or 0.0 wbs_str = str(flat.get("wbs", "")).strip().lower() if f.get("rooms_min") is not None and rooms < float(f["rooms_min"]): failures.add("Zimmer") if f.get("rooms_max") is not None and rooms > float(f["rooms_max"]): failures.add("Zimmer") if f.get("max_rent") is not None and rent > float(f["max_rent"]): failures.add("Preis") if f.get("min_size") is not None and size < float(f["min_size"]): failures.add("Größe") wbs_req = (f.get("wbs_required") or "").strip().lower() if wbs_req == "yes": if not wbs_str or wbs_str in ("kein", "nein", "no", "ohne", "-"): failures.add("WBS") elif wbs_req == "no": if wbs_str and wbs_str not in ("kein", "nein", "no", "ohne", "-"): failures.add("WBS") districts_csv = (f.get("districts") or "").strip() if districts_csv: selected = {d.strip() for d in districts_csv.split(",") if d.strip()} flat_district = (flat.get("district") or "").strip() if not flat_district or flat_district not in selected: failures.add("Bezirk") return [r for r in _REASON_ORDER if r in failures] def flat_matches_filter(flat: dict, f: dict | None) -> bool: """f is a user_filters row converted to dict (or None = no filter set).""" return not flat_filter_failures(flat, f) def row_to_dict(row) -> dict: if row is None: return {} try: return {k: row[k] for k in row.keys()} except Exception: return dict(row)