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:
parent
abd614604f
commit
d13f9c5b6e
7 changed files with 202 additions and 20 deletions
|
|
@ -7,6 +7,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
|||
|
||||
import db
|
||||
from auth import current_user, hash_password, require_csrf, require_user
|
||||
from berlin_districts import DISTRICTS
|
||||
from common import base_context, client_ip, templates
|
||||
from matching import row_to_dict
|
||||
from notifications import telegram_send
|
||||
|
|
@ -40,6 +41,13 @@ def tab_settings(request: Request, section: str):
|
|||
ctx["profile"] = db.get_profile(u["id"])
|
||||
elif section == "filter":
|
||||
ctx["filters"] = row_to_dict(db.get_filters(u["id"]))
|
||||
stored = (ctx["filters"].get("districts") or "").strip()
|
||||
# Empty stored value = no filter = all Bezirke ticked in the UI.
|
||||
ctx["all_districts"] = DISTRICTS
|
||||
ctx["selected_districts"] = (
|
||||
set(DISTRICTS) if not stored
|
||||
else {d.strip() for d in stored.split(",") if d.strip()}
|
||||
)
|
||||
elif section == "benachrichtigungen":
|
||||
ctx["notifications"] = db.get_notifications(u["id"])
|
||||
ctx["notif_flash"] = request.query_params.get("flash") or ""
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from common import (
|
|||
client_ip,
|
||||
templates,
|
||||
)
|
||||
from berlin_districts import DISTRICTS, district_for_address
|
||||
from matching import flat_matches_filter, row_to_dict
|
||||
|
||||
|
||||
|
|
@ -64,6 +65,7 @@ def _wohnungen_context(user) -> dict:
|
|||
if not flat_matches_filter({
|
||||
"rooms": f["rooms"], "total_rent": f["total_rent"], "size": f["size"],
|
||||
"wbs": f["wbs"],
|
||||
"district": district_for_address(f["address"]),
|
||||
}, filters):
|
||||
continue
|
||||
flats_view.append({"row": f, "last": latest_apps.get(f["id"])})
|
||||
|
|
@ -209,18 +211,12 @@ def flat_image(slug: str, index: int):
|
|||
|
||||
|
||||
@router.post("/actions/filters")
|
||||
async def action_save_filters(
|
||||
request: Request,
|
||||
csrf: str = Form(...),
|
||||
rooms_min: str = Form(""),
|
||||
rooms_max: str = Form(""),
|
||||
max_rent: str = Form(""),
|
||||
min_size: str = Form(""),
|
||||
wbs_required: str = Form(""),
|
||||
max_age_hours: str = Form(""),
|
||||
user=Depends(require_user),
|
||||
):
|
||||
require_csrf(user["id"], csrf)
|
||||
async def action_save_filters(request: Request, user=Depends(require_user)):
|
||||
"""The Bezirk filter uses multi-valued `districts` checkboxes, which
|
||||
FastAPI's Form(...) args don't express cleanly — read the whole form
|
||||
via request.form() so we can getlist() on it."""
|
||||
form = await request.form()
|
||||
require_csrf(user["id"], form.get("csrf", ""))
|
||||
|
||||
def _f(v):
|
||||
v = (v or "").strip().replace(",", ".")
|
||||
|
|
@ -233,13 +229,24 @@ async def action_save_filters(
|
|||
except ValueError:
|
||||
return None
|
||||
|
||||
# Canonicalise district selection against the known set. Treat "all
|
||||
# ticked" and "none ticked" identically as "no filter" so the default
|
||||
# and the nuclear-no-results states both mean the same thing.
|
||||
all_districts = set(DISTRICTS)
|
||||
submitted = {d for d in form.getlist("districts") if d in all_districts}
|
||||
if submitted in (set(), all_districts):
|
||||
districts_csv = ""
|
||||
else:
|
||||
districts_csv = ",".join(d for d in DISTRICTS if d in submitted)
|
||||
|
||||
db.update_filters(user["id"], {
|
||||
"rooms_min": _f(rooms_min),
|
||||
"rooms_max": _f(rooms_max),
|
||||
"max_rent": _f(max_rent),
|
||||
"min_size": _f(min_size),
|
||||
"wbs_required": (wbs_required or "").strip(),
|
||||
"max_age_hours": _i(max_age_hours),
|
||||
"rooms_min": _f(form.get("rooms_min", "")),
|
||||
"rooms_max": _f(form.get("rooms_max", "")),
|
||||
"max_rent": _f(form.get("max_rent", "")),
|
||||
"min_size": _f(form.get("min_size", "")),
|
||||
"wbs_required": (form.get("wbs_required") or "").strip(),
|
||||
"max_age_hours": _i(form.get("max_age_hours", "")),
|
||||
"districts": districts_csv,
|
||||
})
|
||||
db.log_audit(user["username"], "filters.updated", user_id=user["id"], ip=client_ip(request))
|
||||
return RedirectResponse("/", status_code=303)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue