Below "Abgelehnte Wohnungen", surface flats that survived the time filter and aren't rejected but failed at least one of the user's filters. Same collapsed-card style. Action buttons are replaced by chips naming each failed dimension — "Zimmer", "Preis", "Größe", "WBS", "Bezirk" — so it's obvious which constraint to relax. Refactored matching: flat_matches_filter now delegates to a new flat_filter_failures(flat, f) that returns the failed-dimension labels (empty list = full match). rooms_min and rooms_max collapse to a single "Zimmer" chip; reasons emit in stable _REASON_ORDER for consistent rendering. The section is suppressed entirely when the user has no filters set, since "everything matches" makes the chips meaningless. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
74 lines
2.7 KiB
Python
74 lines
2.7 KiB
Python
"""
|
|
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)
|